@@ -136,6 +136,8 @@ export type RequestContext = {
136136 retries : number ;
137137 socketErrorRetries : number ;
138138 requestStartTime ?: number ;
139+ redirects : number ;
140+ history : string [ ] ;
139141} ;
140142
141143export 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+
173184export 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
0 commit comments