Skip to content

Commit 0eea26a

Browse files
committed
refactor: auto redirect on the urllib side
1 parent 055b1ec commit 0eea26a

File tree

2 files changed

+101
-25
lines changed

2 files changed

+101
-25
lines changed

src/HttpClient.ts

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export type RequestContext = {
136136
retries: number;
137137
socketErrorRetries: number;
138138
requestStartTime?: number;
139+
redirects: number;
140+
history: string[];
139141
};
140142

141143
export const channels = {
@@ -170,6 +172,15 @@ export interface PoolStat {
170172
size: number;
171173
}
172174

175+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
176+
const RedirectStatusCodes = [
177+
301, // Moved Permanently
178+
302, // Found
179+
303, // See Other
180+
307, // Temporary Redirect
181+
308, // Permanent Redirect
182+
];
183+
173184
export class HttpClient extends EventEmitter {
174185
#defaultArgs?: RequestOptions;
175186
#dispatcher?: Dispatcher;
@@ -274,11 +285,14 @@ export class HttpClient extends EventEmitter {
274285
requestContext = {
275286
retries: 0,
276287
socketErrorRetries: 0,
288+
redirects: 0,
289+
history: [],
277290
...requestContext,
278291
};
279292
if (!requestContext.requestStartTime) {
280293
requestContext.requestStartTime = performance.now();
281294
}
295+
requestContext.history.push(requestUrl.href);
282296
const requestStartTime = requestContext.requestStartTime;
283297

284298
// https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
@@ -338,7 +352,7 @@ export class HttpClient extends EventEmitter {
338352
aborted: false,
339353
rt: 0,
340354
keepAliveSocket: true,
341-
requestUrls: [],
355+
requestUrls: requestContext.history,
342356
timing,
343357
socket: socketInfo,
344358
retries: requestContext.retries,
@@ -399,10 +413,13 @@ export class HttpClient extends EventEmitter {
399413
isStreamingRequest = true;
400414
}
401415

416+
let maxRedirects = args.maxRedirects ?? 10;
417+
402418
try {
403419
const requestOptions: IUndiciRequestOption = {
404420
method,
405-
maxRedirections: args.maxRedirects ?? 10,
421+
// disable undici auto redirect handler
422+
maxRedirections: 0,
406423
headersTimeout,
407424
headers,
408425
bodyTimeout,
@@ -417,7 +434,7 @@ export class HttpClient extends EventEmitter {
417434
requestOptions.reset = args.reset;
418435
}
419436
if (args.followRedirect === false) {
420-
requestOptions.maxRedirections = 0;
437+
maxRedirects = 0;
421438
}
422439

423440
const isGETOrHEAD = requestOptions.method === 'GET' || requestOptions.method === 'HEAD';
@@ -545,8 +562,8 @@ export class HttpClient extends EventEmitter {
545562
args.socketErrorRetry = 0;
546563
}
547564

548-
debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s, maxRedirections: %s',
549-
requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest, requestOptions.maxRedirections);
565+
debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s, maxRedirections: %s, redirects: %s',
566+
requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest, maxRedirects, requestContext.redirects);
550567
requestOptions.headers = headers;
551568
channels.request.publish({
552569
request: reqMeta,
@@ -577,18 +594,6 @@ export class HttpClient extends EventEmitter {
577594
response = await undiciRequest(requestUrl, requestOptions as UndiciRequestOption);
578595
}
579596
}
580-
581-
const context = response.context as { history: URL[] };
582-
let lastUrl = '';
583-
if (context?.history) {
584-
for (const urlObject of context?.history) {
585-
res.requestUrls.push(urlObject.href);
586-
lastUrl = urlObject.href;
587-
}
588-
} else {
589-
res.requestUrls.push(requestUrl.href);
590-
lastUrl = requestUrl.href;
591-
}
592597
const contentEncoding = response.headers['content-encoding'];
593598
const isCompressedContent = contentEncoding === 'gzip' || contentEncoding === 'br';
594599

@@ -599,6 +604,19 @@ export class HttpClient extends EventEmitter {
599604
res.size = parseInt(res.headers['content-length']);
600605
}
601606

607+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
608+
if (RedirectStatusCodes.includes(res.statusCode) && maxRedirects > 0 && requestContext.redirects < maxRedirects && !isStreamingRequest) {
609+
if (res.headers.location) {
610+
requestContext.redirects++;
611+
const nextUrl = new URL(res.headers.location, requestUrl.href);
612+
// Ensure the response is consumed
613+
await response.body.arrayBuffer();
614+
debug('Request#%d got response, status: %s, headers: %j, timing: %j, redirect to %s',
615+
requestId, res.status, res.headers, res.timing, nextUrl.href);
616+
return await this.#requestInternal(nextUrl.href, options, requestContext);
617+
}
618+
}
619+
602620
let data: any = null;
603621
if (args.dataType === 'stream') {
604622
// only auto decompress on request args.compressed = true
@@ -650,12 +668,15 @@ export class HttpClient extends EventEmitter {
650668
statusCode: res.status,
651669
statusText: res.statusText,
652670
headers: res.headers,
653-
url: lastUrl,
654-
redirected: res.requestUrls.length > 1,
671+
url: requestUrl.href,
672+
redirected: requestContext.history.length > 1,
655673
requestUrls: res.requestUrls,
656674
res,
657675
};
658676

677+
debug('Request#%d got response, status: %s, headers: %j, timing: %j',
678+
requestId, res.status, res.headers, res.timing);
679+
659680
if (args.retry > 0 && requestContext.retries < args.retry) {
660681
const isRetry = args.isRetry ?? defaultIsRetry;
661682
if (isRetry(clientResponse)) {
@@ -667,8 +688,6 @@ export class HttpClient extends EventEmitter {
667688
}
668689
}
669690

670-
debug('Request#%d got response, status: %s, headers: %j, timing: %j',
671-
requestId, res.status, res.headers, res.timing);
672691
channels.response.publish({
673692
request: reqMeta,
674693
response: res,
@@ -715,10 +734,6 @@ export class HttpClient extends EventEmitter {
715734
err._rawSocket = err.socket;
716735
}
717736
err.socket = socketInfo;
718-
// make sure requestUrls not empty
719-
if (res.requestUrls.length === 0) {
720-
res.requestUrls.push(requestUrl.href);
721-
}
722737
res.rt = performanceTime(requestStartTime);
723738
updateSocketInfo(socketInfo, internalOpaque, rawError);
724739

test/HttpClient.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,67 @@ describe('HttpClient.test.ts', () => {
170170
// console.log(response.res.socket, response.res.timing);
171171
assert.equal(response.data, 'hello h2!');
172172
});
173+
174+
it('should auto redirect work', async () => {
175+
const server = createSecureServer(pem);
176+
177+
let count = 0;
178+
server.on('stream', (stream, headers) => {
179+
count++;
180+
// console.log(count, headers);
181+
if (count === 2) {
182+
stream.respond({
183+
'content-type': 'text/plain; charset=utf-8',
184+
'x-custom-h2': 'hello',
185+
location: '/see-other',
186+
':status': 302,
187+
});
188+
stream.end();
189+
return;
190+
}
191+
assert.equal(headers[':method'], 'GET');
192+
stream.respond({
193+
'content-type': 'text/plain; charset=utf-8',
194+
'x-custom-h2': 'hello',
195+
':status': 200,
196+
});
197+
stream.end('hello h2!');
198+
});
199+
200+
server.listen(0);
201+
await once(server, 'listening');
202+
203+
const httpClient = new HttpClient({
204+
allowH2: true,
205+
connect: {
206+
rejectUnauthorized: false,
207+
},
208+
});
209+
210+
const url = `https://localhost:${server.address()!.port}`;
211+
let response = await httpClient.request<string>(url, {
212+
dataType: 'text',
213+
headers: {
214+
'x-my-header': 'foo',
215+
},
216+
});
217+
assert.equal(response.status, 200);
218+
assert.equal(response.headers['x-custom-h2'], 'hello');
219+
// console.log(response.res.socket, response.res.timing);
220+
assert.equal(response.data, 'hello h2!');
221+
await sleep(200);
222+
response = await httpClient.request<string>(url, {
223+
dataType: 'text',
224+
headers: {
225+
'x-my-header': 'foo2',
226+
},
227+
followRedirect: true,
228+
});
229+
assert.equal(response.status, 200);
230+
assert.equal(response.headers['x-custom-h2'], 'hello');
231+
// console.log(response.res.socket, response.res.timing);
232+
assert.equal(response.data, 'hello h2!');
233+
});
173234
});
174235

175236
describe('clientOptions.defaultArgs', () => {

0 commit comments

Comments
 (0)