@@ -80,9 +80,36 @@ export interface CommonHttpClientOptions {
80
80
*/
81
81
processError ?: ( error : Error ) => Error ;
82
82
/**
83
- * Whether to follow redirects. Default is true .
83
+ * External fetch method. Will be used for external redirects .
84
84
*/
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
+ > ) ;
86
113
}
87
114
88
115
/**
@@ -643,6 +670,59 @@ const formatParameter: Record<CommonHttpClientRequestParameterSerializeStyle, Pa
643
670
*/
644
671
const deprecationWarningShown : { [ methodAndPath : string ] : boolean } = { } ;
645
672
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
+
646
726
/**
647
727
* Common HTTP client. Configurable for different environments.
648
728
*/
@@ -749,32 +829,77 @@ export class CommonHttpClient {
749
829
return url ;
750
830
}
751
831
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 ) ;
768
897
}
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 ) ;
778
903
}
779
904
780
905
/**
@@ -784,37 +909,11 @@ export class CommonHttpClient {
784
909
try {
785
910
return await this . performRequest ( request ) ;
786
911
} 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 ) ;
810
913
}
811
914
}
812
915
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 > {
818
917
try {
819
918
request = await this . preprocessRequest ( request ) ;
820
919
} catch ( e ) {
@@ -838,18 +937,6 @@ export class CommonHttpClient {
838
937
`preprocessRequest error: ${ getErrorMessage ( e ) } `
839
938
) ;
840
939
}
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
- }
853
940
const {
854
941
body,
855
942
path : _path ,
@@ -861,24 +948,27 @@ export class CommonHttpClient {
861
948
...otherRequestProps
862
949
} = request ;
863
950
const headers = this . cleanupHeaders ( requestHeaders ) ;
864
- const fetchRequest : CommonHttpClientFetchRequest = {
951
+ return {
865
952
...otherRequestProps ,
866
953
headers,
867
954
cache : cache ?? 'default' ,
868
955
credentials : credentials ?? 'same-origin' ,
869
956
redirect : 'error' ,
870
957
body : this . getRequestBody ( request )
871
958
} ;
959
+ }
960
+
961
+ protected async performFetchRequest (
962
+ url : URL ,
963
+ fetchRequest : CommonHttpClientFetchRequest ,
964
+ fetchMethod : ( url : URL , request : CommonHttpClientFetchRequest ) => Promise < CommonHttpClientFetchResponse >
965
+ ) : Promise < CommonHttpClientFetchResponse > {
872
966
let attemptNumber = 1 ;
873
967
for ( ; ; ) {
874
968
try {
875
969
let fetchResponse : CommonHttpClientFetchResponse ;
876
970
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 ) ;
882
972
} catch ( e ) {
883
973
throw new this . options . errorClass ( url , fetchRequest , undefined , this . options , getErrorMessage ( e ) ) ;
884
974
}
@@ -903,7 +993,7 @@ export class CommonHttpClient {
903
993
this . options ,
904
994
this . options . formatHttpErrorMessage
905
995
? 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 } )`
907
997
) ;
908
998
}
909
999
return fetchResponse ;
@@ -916,6 +1006,27 @@ export class CommonHttpClient {
916
1006
}
917
1007
}
918
1008
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
+
919
1030
/**
920
1031
* Post-process the response.
921
1032
*/
0 commit comments