@@ -80,9 +80,36 @@ export interface CommonHttpClientOptions {
8080 */
8181 processError ?: ( error : Error ) => Error ;
8282 /**
83- * Whether to follow redirects. Default is true .
83+ * External fetch method. Will be used for external redirects .
8484 */
85- followRedirects ?: boolean ;
85+ externalFetch ?: ( url : URL , request : CommonHttpClientFetchRequest ) => Promise < CommonHttpClientFetchResponse > ;
86+ /**
87+ * Whether to follow redirects. Default is true. Can also be a function that decides what to do on a redirect.
88+ */
89+ followRedirects ?:
90+ | boolean
91+ | ( ( params : {
92+ url : URL ;
93+ request : CommonHttpClientFetchRequest ;
94+ response : CommonHttpClientFetchResponse ;
95+ } ) => Promise <
96+ | {
97+ type : 'error' ;
98+ error ?: Error ;
99+ }
100+ | {
101+ type : 'response' ;
102+ response : CommonHttpClientFetchResponse ;
103+ }
104+ | {
105+ type : 'redirect' ;
106+ request ?: CommonHttpClientFetchRequest ;
107+ }
108+ | {
109+ type : 'externalRedirect' ;
110+ request ?: CommonHttpClientFetchRequest ;
111+ }
112+ > ) ;
86113}
87114
88115/**
@@ -643,6 +670,59 @@ const formatParameter: Record<CommonHttpClientRequestParameterSerializeStyle, Pa
643670 */
644671const deprecationWarningShown : { [ methodAndPath : string ] : boolean } = { } ;
645672
673+ /**
674+ * Default implementation of the redirect handler.
675+ */
676+ const defaultRedirectHandler : Exclude < CommonHttpClientOptions [ 'followRedirects' ] , boolean | undefined > = async ( {
677+ url,
678+ response
679+ } : {
680+ url : URL ;
681+ response : CommonHttpClientFetchResponse ;
682+ } ) => {
683+ const redirectUrl = new URL ( response . headers [ 'location' ] , url ) ;
684+ let responseUrl ;
685+ try {
686+ responseUrl = new URL ( response . url ) ;
687+ } catch ( e ) {
688+ responseUrl = url ;
689+ }
690+
691+ if ( responseUrl . host !== redirectUrl . host ) {
692+ return { type : 'externalRedirect' } ;
693+ } else {
694+ return { type : 'redirect' } ;
695+ }
696+ } ;
697+
698+ /**
699+ * Default fetch implementation.
700+ */
701+ async function defaultFetch ( url : URL , request : CommonHttpClientFetchRequest ) : Promise < CommonHttpClientFetchResponse > {
702+ const { ...requestProps } = request ;
703+ const requestInit : RequestInit = requestProps ;
704+ const response = await fetch ( url , requestInit ) ;
705+ const body : CommonHttpClientFetchResponseBody = isJsonMediaType ( response . headers . get ( 'content-type' ) ?? '' )
706+ ? { type : 'json' , data : await response . json ( ) }
707+ : { type : 'blob' , data : await response . blob ( ) } ;
708+ const headers : CommonHttpClientResponseHeaders = { } ;
709+ response . headers . forEach ( ( value , key ) => {
710+ headers [ key ] = value ;
711+ } ) ;
712+ if ( response . headers . has ( 'set-cookie' ) && 'getSetCookie' in response . headers ) {
713+ headers [ 'set-cookie' ] = ( response . headers as { getSetCookie ( ) : string [ ] } ) . getSetCookie ( ) ;
714+ }
715+ return {
716+ status : response . status ,
717+ statusText : response . statusText ,
718+ body,
719+ url : response . url ,
720+ headers,
721+ ok : response . ok ,
722+ customRequestProps : request . customRequestProps
723+ } ;
724+ }
725+
646726/**
647727 * Common HTTP client. Configurable for different environments.
648728 */
@@ -749,32 +829,77 @@ export class CommonHttpClient {
749829 return url ;
750830 }
751831
752- /**
753- * Default fetch implementation.
754- */
755- protected async fetch ( url : URL , request : CommonHttpClientFetchRequest ) : Promise < CommonHttpClientFetchResponse > {
756- const { ...requestProps } = request ;
757- const requestInit : RequestInit = requestProps ;
758- const response = await fetch ( url , requestInit ) ;
759- const body : CommonHttpClientFetchResponseBody = isJsonMediaType ( response . headers . get ( 'content-type' ) ?? '' )
760- ? { type : 'json' , data : await response . json ( ) }
761- : { type : 'blob' , data : await response . blob ( ) } ;
762- const headers : CommonHttpClientResponseHeaders = { } ;
763- response . headers . forEach ( ( value , key ) => {
764- headers [ key ] = value ;
765- } ) ;
766- if ( response . headers . has ( 'set-cookie' ) && 'getSetCookie' in response . headers ) {
767- headers [ 'set-cookie' ] = ( response . headers as { getSetCookie ( ) : string [ ] } ) . getSetCookie ( ) ;
832+ protected processErrorIfNecessary ( error : unknown ) {
833+ if ( this . options . processError ) {
834+ return this . options . processError ( error instanceof Error ? error : new Error ( String ( error ) ) ) ;
835+ }
836+ return error ;
837+ }
838+
839+ protected async handleRedirect ( error : CommonHttpClientError ) {
840+ if ( this . options . followRedirects === false ) {
841+ throw this . processErrorIfNecessary ( error ) ;
842+ }
843+
844+ const { request, response, url} = error ;
845+
846+ if ( ! request || ! response ) {
847+ throw this . processErrorIfNecessary ( error ) ;
848+ }
849+
850+ if ( response . status <= 300 || response . status >= 400 || ! response . headers [ 'location' ] ) {
851+ throw this . processErrorIfNecessary ( error ) ;
852+ }
853+
854+ const redirectHandler =
855+ typeof this . options . followRedirects === 'function' ? this . options . followRedirects : defaultRedirectHandler ;
856+
857+ const action = await redirectHandler ( { url, request, response} ) ;
858+
859+ if ( ! action || ! ( 'type' in action ) ) {
860+ error . message = `Invalid redirect handler result for ${ error . message } .` ;
861+ throw this . processErrorIfNecessary ( error ) ;
862+ }
863+
864+ const redirectPreservingMethod = response . status === 307 || response . status === 308 ;
865+ const newUrl = new URL ( response . headers [ 'location' ] , url ) ;
866+
867+ if ( action . type === 'error' ) {
868+ error . message = `Redirect to ${ newUrl . toString ( ) } not allowed by redirect handler. ${ error . message } ` ;
869+ throw this . processErrorIfNecessary ( action . error ?? error ) ;
870+ } else if ( action . type === 'response' ) {
871+ return action . response ;
872+ } else if ( action . type === 'redirect' ) {
873+ const fetchRequest =
874+ action . request ??
875+ ( await this . generateFetchRequest ( {
876+ path : newUrl . pathname ,
877+ method : redirectPreservingMethod ? request . method : 'GET'
878+ } ) ) ;
879+ return this . performFetchRequest ( newUrl , fetchRequest , this . options . fetch ?? defaultFetch ) . catch ( ( error ) =>
880+ this . handleRequestError ( error )
881+ ) ;
882+ } else if ( action . type === 'externalRedirect' ) {
883+ const fetchRequest = action . request ?? {
884+ // Change method to GET for 301, 302, 303 redirects
885+ method : redirectPreservingMethod ? request . method : 'GET' ,
886+ headers : { } ,
887+ cache : request . cache ,
888+ credentials : request . credentials ,
889+ redirect : 'error'
890+ } ;
891+ return this . performFetchRequest ( newUrl , fetchRequest , this . options . externalFetch ?? defaultFetch ) . catch (
892+ ( error ) => this . handleRequestError ( error )
893+ ) ;
894+ } else {
895+ error . message = `Invalid redirect handler result for ${ error . message } .` ;
896+ throw this . processErrorIfNecessary ( error ) ;
768897 }
769- return {
770- status : response . status ,
771- statusText : response . statusText ,
772- body,
773- url : response . url ,
774- headers,
775- ok : response . ok ,
776- customRequestProps : request . customRequestProps
777- } ;
898+ }
899+
900+ protected handleRequestError ( e : unknown ) : never | Promise < CommonHttpClientFetchResponse > {
901+ const error = e as CommonHttpClientError ;
902+ return this . handleRedirect ( error ) ;
778903 }
779904
780905 /**
@@ -784,37 +909,11 @@ export class CommonHttpClient {
784909 try {
785910 return await this . performRequest ( request ) ;
786911 } catch ( e ) {
787- const error = e as CommonHttpClientError ;
788- if ( error . response ) {
789- if (
790- error . response . status > 300 &&
791- error . response . status < 400 &&
792- error . response . headers [ 'location' ] &&
793- this . options . followRedirects !== false
794- ) {
795- const redirectUrl = new URL ( error . response . headers [ 'location' ] , error . url ) ;
796- return this . request ( {
797- method : error . response . status === 307 || error . response . status === 308 ? request . method : 'GET' ,
798- path : redirectUrl . pathname ,
799- query :
800- redirectUrl . searchParams . size > 0
801- ? Object . fromEntries ( redirectUrl . searchParams . entries ( ) )
802- : undefined
803- } ) ;
804- }
805- }
806- if ( this . options . processError ) {
807- throw this . options . processError ( e instanceof Error ? e : new Error ( String ( e ) ) ) ;
808- }
809- throw e ;
912+ return await this . handleRequestError ( e ) ;
810913 }
811914 }
812915
813- /**
814- * Perform a request.
815- */
816- protected async performRequest ( request : CommonHttpClientRequest ) : Promise < CommonHttpClientFetchResponse > {
817- this . logDeprecationWarningIfNecessary ( request ) ;
916+ protected async generateFetchRequest ( request : CommonHttpClientRequest ) : Promise < CommonHttpClientFetchRequest > {
818917 try {
819918 request = await this . preprocessRequest ( request ) ;
820919 } catch ( e ) {
@@ -838,18 +937,6 @@ export class CommonHttpClient {
838937 `preprocessRequest error: ${ getErrorMessage ( e ) } `
839938 ) ;
840939 }
841- let url ;
842- try {
843- url = this . buildUrl ( request ) ;
844- } catch ( e ) {
845- throw new this . options . errorClass (
846- new URL ( request . path , this . options . baseUrl ) ,
847- undefined ,
848- undefined ,
849- this . options ,
850- `Error building request URL: ${ getErrorMessage ( e ) } `
851- ) ;
852- }
853940 const {
854941 body,
855942 path : _path ,
@@ -861,24 +948,27 @@ export class CommonHttpClient {
861948 ...otherRequestProps
862949 } = request ;
863950 const headers = this . cleanupHeaders ( requestHeaders ) ;
864- const fetchRequest : CommonHttpClientFetchRequest = {
951+ return {
865952 ...otherRequestProps ,
866953 headers,
867954 cache : cache ?? 'default' ,
868955 credentials : credentials ?? 'same-origin' ,
869956 redirect : 'error' ,
870957 body : this . getRequestBody ( request )
871958 } ;
959+ }
960+
961+ protected async performFetchRequest (
962+ url : URL ,
963+ fetchRequest : CommonHttpClientFetchRequest ,
964+ fetchMethod : ( url : URL , request : CommonHttpClientFetchRequest ) => Promise < CommonHttpClientFetchResponse >
965+ ) : Promise < CommonHttpClientFetchResponse > {
872966 let attemptNumber = 1 ;
873967 for ( ; ; ) {
874968 try {
875969 let fetchResponse : CommonHttpClientFetchResponse ;
876970 try {
877- if ( this . options . fetch ) {
878- fetchResponse = await this . options . fetch ( url , fetchRequest ) ;
879- } else {
880- fetchResponse = await this . fetch ( url , fetchRequest ) ;
881- }
971+ fetchResponse = await fetchMethod ( url , fetchRequest ) ;
882972 } catch ( e ) {
883973 throw new this . options . errorClass ( url , fetchRequest , undefined , this . options , getErrorMessage ( e ) ) ;
884974 }
@@ -903,7 +993,7 @@ export class CommonHttpClient {
903993 this . options ,
904994 this . options . formatHttpErrorMessage
905995 ? this . options . formatHttpErrorMessage ( fetchResponse , fetchRequest )
906- : `HTTP Error ${ request . method } ${ url . toString ( ) } ${ fetchResponse . status } (${ fetchResponse . statusText } )`
996+ : `HTTP Error ${ fetchRequest . method } ${ url . toString ( ) } ${ fetchResponse . status } (${ fetchResponse . statusText } )`
907997 ) ;
908998 }
909999 return fetchResponse ;
@@ -916,6 +1006,27 @@ export class CommonHttpClient {
9161006 }
9171007 }
9181008
1009+ /**
1010+ * Perform a request.
1011+ */
1012+ protected async performRequest ( request : CommonHttpClientRequest ) : Promise < CommonHttpClientFetchResponse > {
1013+ this . logDeprecationWarningIfNecessary ( request ) ;
1014+ const fetchRequest = await this . generateFetchRequest ( request ) ;
1015+ let url ;
1016+ try {
1017+ url = this . buildUrl ( request ) ;
1018+ } catch ( e ) {
1019+ throw new this . options . errorClass (
1020+ new URL ( request . path , this . options . baseUrl ) ,
1021+ undefined ,
1022+ undefined ,
1023+ this . options ,
1024+ `Error building request URL: ${ getErrorMessage ( e ) } `
1025+ ) ;
1026+ }
1027+ return this . performFetchRequest ( url , fetchRequest , this . options . fetch ?? defaultFetch ) ;
1028+ }
1029+
9191030 /**
9201031 * Post-process the response.
9211032 */
0 commit comments