Comments (7)
It would be easiest if ProxyAgent
just exposes an appropriate callback for this in this style that I linked in the description. But as I need something right now, I tried kludging stuff around by Dispatcher.compose
a RetryHandler
into the Pool
used to establish the proxy connection via the clientFactory
option (As retrying the request by myself via the dispatch mechanism seems difficult, but there is likely a much simpler way to do this for this case, as I don't need the entirety of the RetryHandler, just the ability to retry the request)
Sadly it doesn't handle onUpgrade
so I also add to extend it to handle 407 and error appropriately. I naively tried to compose a handler after the RetryHandler to throw an error but that causes RetryHandler
to abort and not retry, so had to extend it.
I'll post my code here for reference once I have cleaned it up. Hopefully it will serve as inspiration on how to actually implement this correctly.
P.S. I have hit a lot of missing types (TypeScript):
clientFactory
forProxyAgent
errors.RequestRetryError
opts.servername
RetryHandler.abort
RetryHandler.retryOpts
RetryHandler.retryCount
RetryHandler.handler
- Whether you extend
DecoratorHandler
or any other handler, TypeScript doesn't recognize any of the methods that you can override, or that you can call them viasuper
, e.g.onUpgrade
,onHeaders
.
from undici.
Here is what I managed to cobble together so far:
First version
import stream from 'node:stream';
import { Dispatcher, EnvHttpProxyAgent, Pool, RetryHandler, errors, setGlobalDispatcher, util } from 'undici';
import { IncomingHttpHeaders } from 'undici/types/header';
class RetryHandler2 extends RetryHandler {
declare retryCount: number;
declare retryOpts: Required<RetryHandler.RetryOptions>;
declare handler: Required<Dispatcher.DispatchHandlers>;
declare abort: (err?: Error) => void;
onUpgrade(statusCode: number, headers: Buffer[] | string[] | null, socket: stream.Duplex) {
this.retryCount++;
if (statusCode >= 300) {
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
return this.handler.onUpgrade(statusCode, headers, socket);
} else {
this.abort(
// @ts-expect-error Missing types
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
new errors.RequestRetryError('Request failed', statusCode, {
headers,
count: this.retryCount,
})
);
return false;
}
}
// @ts-expect-error Missing types
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return super.onUpgrade(statusCode, headers, socket);
}
}
setGlobalDispatcher(
// XXX Will emit an experimental warning
new EnvHttpProxyAgent({
// @ts-expect-error clientFactory missing in types
clientFactory: (origin: URL, opts: object): Dispatcher =>
new Pool(origin, opts).compose([
(dispatch) => {
return function interceptedDispatch(opts, handler) {
return dispatch(
opts,
new RetryHandler2(
{
...opts,
retryOptions: {
retry: function (err, state, callback) {
// @ts-expect-error Missing types
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
if (err instanceof errors.RequestRetryError && err.statusCode === 407) {
// @ts-expect-error Missing types
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const headers = util.parseHeaders(err.headers);
const header = headers['proxy-authenticate'];
const authenticate = Array.isArray(header)
? header
: typeof header === 'string'
? [header]
: [];
if (authenticate.some((a) => /^(Negotiate|Kerberos)( |$)/i.test(a))) {
import('kerberos')
.then(async ({ default: kerberos }) => {
const spn =
// @ts-expect-error Missing types
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
process.platform === 'win32' ? `HTTP/${opts.servername}` : `HTTP@${opts.servername}`;
const client = await kerberos.initializeClient(spn);
const response = await client.step('');
(state.opts.headers as IncomingHttpHeaders)['proxy-authorization'] =
'Negotiate ' + response;
callback(null);
})
.catch(callback);
return null;
}
}
callback(err);
return null;
},
maxRetries: 1,
statusCodes: [407],
errorCodes: [],
retryAfter: false,
},
},
{
handler,
dispatch,
}
)
);
};
},
]),
})
);
Second version
// #region RetryHandler2
class RetryHandler2 extends RetryHandler {
declare retryCount: number;
declare retryOpts: Required<RetryHandler.RetryOptions>;
declare handler: Required<Dispatcher.DispatchHandlers>;
declare abort: (err?: Error) => void;
onUpgrade(statusCode: number, headers: Buffer[] | string[] | null, socket: stream.Duplex) {
this.retryCount++;
if (statusCode >= 300) {
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
return this.handler.onUpgrade(statusCode, headers, socket);
} else {
this.abort(
// @ts-expect-error Missing types for errors.RequestRetryError
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
new errors.RequestRetryError('Request failed', statusCode, {
headers,
count: this.retryCount,
})
);
return false;
}
}
// @ts-expect-error Missing types for RetryHandler actually implemented onUpgrade
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return super.onUpgrade(statusCode, headers, socket);
}
}
// //#endregion RetryHandler2
function kerberosInterceptor(dispatch: Dispatcher['dispatch']): Dispatcher['dispatch'] {
return function kerberosInterceptor(opts, handler) {
// TODO We can try to cache the Proxy-Authenticate header like we do in
// lookupProxyAuthorization, and pre-emptively send Negotiate rather than
// get hit with a 407 each time
return dispatch(
opts,
new RetryHandler2(
{
...opts,
retryOptions: {
retry: function (err, state, callback) {
// @ts-expect-error Missing types for errors.RequestRetryError
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
if (err instanceof errors.RequestRetryError && err.statusCode === 407) {
// @ts-expect-error Missing types for errors.RequestRetryError
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const headers = util.parseHeaders(err.headers);
const header = headers['proxy-authenticate'];
const authenticate = Array.isArray(header) ? header : typeof header === 'string' ? [header] : [];
if (authenticate.some((a) => /^(Negotiate|Kerberos)( |$)/i.test(a))) {
import('kerberos')
.then(async ({ default: kerberos }) => {
const spn =
// @ts-expect-error Missing types for opts.servername
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
process.platform === 'win32' ? `HTTP/${opts.servername}` : `HTTP@${opts.servername}`;
// TODO You are technically supposed to pass mechOID based on whether it is Negotiate or Kerberos
const client = await kerberos.initializeClient(spn);
// TODO When falling back to NTLM in Negotitate, you need multiple steps
const response = await client.step('');
(state.opts.headers as IncomingHttpHeaders)['proxy-authorization'] = 'Negotiate ' + response;
callback(null);
})
.catch(callback);
return null;
}
}
callback(err);
return null;
},
maxRetries: 1,
statusCodes: [407],
errorCodes: [],
retryAfter: false,
},
},
{
handler,
dispatch,
}
)
);
};
}
from undici.
Nice work, I believe this can live as a standalone npm
package that exposes this interceptor.
from undici.
Yeah. But the code feels a bit kludgey for now... Maybe a different kind of handler than RetryHandler2
to handle this, so it's cleaner? Like an AuthenticationHandler
, that one could possibly live in undici
? with the actual callback that does authentication being in a separate npm package? I kinda used RetryHandler
like that cause I wasn't sure how to implement my own handler that does retries when writing it, but it should be possible to create a more condescend one to only handle 401/407. But one thing I'm not sure about is keeping per request state in such handlers, does a handler not get called concurrently if there are multiple async requests going on at the same time? Wouldn't that mess up the state the handler keeps on this
such as retryCount
?
from undici.
I can see an interceptor for authentication, but it will require to be generic rather than scoped to an specific technology.
You can customize RetryHandler
as you need 👍
And the handlers are made to be scoped per request, so each handler should preserve the state of a single request; the same applies to the retry handler, the request that triggered the handler will be scoped to it, if a subsequent request is made another handler will be instantiated.
from undici.
Yeah, the idea is a generic authentication handler that is like retry handler but only for handling 401 or 407 that receives a callback for the actual handling of the auth method.
from undici.
Got it, a mix of Retry and Proxy-Like agent; it could be a good package, not so sure of undici core is the place for that.
It can make usage of the RetryHandler extending it as you provided in your examples; possibly extending the handler to call the retry
callback with the dispatch opts so it can overwrite it when it detects a 401
or 407
(tho I'd focus lonely in 407
as 401
is something that should be known ahead of time)
from undici.
Related Issues (20)
- Does not set TLS `servername` to the value of the `Host` header HOT 9
- Significant Slowdown in Requests When Using ProxyAgent HOT 4
- inconsistent setDate(0) behaviour between windows and linux HOT 1
- Nightly tests are failing HOT 6
- `UND_ERR_CONNECT_TIMEOUT` errors thrown when there is CPU intensive code on the event loop HOT 6
- Expose "Content-Encoding" handling publicly HOT 11
- Update automated release scripts to release from the v6.x branch
- Missing `6.19.4` tag/github release? HOT 1
- DELETED HOT 6
- `fetch` hangs indefinitely during second request to a certain webpage when using HTTP/2 HOT 4
- `undici.fetch` does not implement `body.dump()` as similar to `undici.request` (`Uncaught TypeError: response.body.dump is not a function`) HOT 1
- ERR_TLS_CERT_ALTNAME_INVALID error when using the proxy support (ProxyAgent) HOT 1
- react-scripts build is failing with TS1005 for balanced-pool.d.ts type HOT 5
- Can't use Undici with Brightdata proxy (error 407) HOT 1
- MockAgent not compatible with ProxyAgent,EnvHttpProxyAgent,RetryAgent HOT 2
- I do not receive set-cookie in response.headers.getSetCookie() HOT 5
- Missing bytes() mixin in response.body from request method HOT 1
- how to use maxRedirections in fetch api HOT 1
- WebSocket Data Transfer Slows Down After Node.js 16.14.2 Release
- code error HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from undici.