1- import { Buffer } from 'node:buffer' ;
21import { EventEmitter } from 'node:events' ;
32import is from '@sindresorhus/is' ;
43import {
54 HTTPError ,
65 RetryError ,
76 type RequestError ,
87} from '../core/errors.js' ;
9- import Request from '../core/index.js' ;
8+ import Request , { normalizeError } from '../core/index.js' ;
109import {
10+ decodeUint8Array ,
11+ isUtf8Encoding ,
1112 parseBody ,
1213 isResponseOk ,
1314 type Response , ParseError ,
1415} from '../core/response.js' ;
1516import proxyEvents from '../core/utils/proxy-events.js' ;
17+ import {
18+ applyUrlOverride ,
19+ isSameOrigin ,
20+ snapshotCrossOriginState ,
21+ } from '../core/options.js' ;
1622import type Options from '../core/options.js' ;
1723import { type RequestPromise } from './types.js' ;
1824
25+ const compressedEncodings = new Set ( [ 'gzip' , 'deflate' , 'br' , 'zstd' ] ) ;
26+
1927const proxiedRequestEvents = [
2028 'request' ,
2129 'response' ,
@@ -24,34 +32,6 @@ const proxiedRequestEvents = [
2432 'downloadProgress' ,
2533] ;
2634
27- const normalizeError = ( error : unknown ) : Error => {
28- if ( error instanceof Error ) {
29- return error ;
30- }
31-
32- if ( is . object ( error ) ) {
33- const errorLike = error as Partial < Error & { code ?: string ; input ?: string } > ;
34- const message = typeof errorLike . message === 'string' ? errorLike . message : 'Non-error object thrown' ;
35- const normalizedError = new Error ( message , { cause : error } ) as Error & { code ?: string ; input ?: string } ;
36-
37- if ( typeof errorLike . stack === 'string' ) {
38- normalizedError . stack = errorLike . stack ;
39- }
40-
41- if ( typeof errorLike . code === 'string' ) {
42- normalizedError . code = errorLike . code ;
43- }
44-
45- if ( typeof errorLike . input === 'string' ) {
46- normalizedError . input = errorLike . input ;
47- }
48-
49- return normalizedError ;
50- }
51-
52- return new Error ( String ( error ) ) ;
53- } ;
54-
5535export default function asPromise < T > ( firstRequest ?: Request ) : RequestPromise < T > {
5636 let globalRequest : Request ;
5737 let globalResponse : Response ;
@@ -69,7 +49,7 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
6949 request . once ( 'response' , async ( response : Response ) => {
7050 // Parse body
7151 const contentEncoding = ( response . headers [ 'content-encoding' ] ?? '' ) . toLowerCase ( ) ;
72- const isCompressed = contentEncoding === 'gzip' || contentEncoding === 'deflate' || contentEncoding === 'br' || contentEncoding === 'zstd' ;
52+ const isCompressed = compressedEncodings . has ( contentEncoding ) ;
7353
7454 const { options} = request ;
7555
@@ -81,7 +61,7 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
8161 } catch ( error : unknown ) {
8262 // Fall back to `utf8`
8363 try {
84- response . body = Buffer . from ( response . rawBody ) . toString ( ) ;
64+ response . body = decodeUint8Array ( response . rawBody ) ;
8565 } catch ( error ) {
8666 request . _beforeError ( new ParseError ( normalizeError ( error ) , response ) ) ;
8767 return ;
@@ -98,16 +78,46 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
9878 const hooks = options . hooks . afterResponse ;
9979
10080 for ( const [ index , hook ] of hooks . entries ( ) ) {
81+ const previousUrl = options . url ? new URL ( options . url ) : undefined ;
82+ const previousState = previousUrl ? snapshotCrossOriginState ( options ) : undefined ;
83+ const requestOptions = response . request . options ;
84+ const responseSnapshot = response ;
85+
10186 // @ts -expect-error TS doesn't notice that RequestPromise is a Promise
10287 // eslint-disable-next-line no-await-in-loop
103- response = await hook ( response , async ( updatedOptions ) : RequestPromise < Response > => {
88+ response = await requestOptions . trackStateMutations ( async changedState => hook ( responseSnapshot , async ( updatedOptions ) : RequestPromise < Response > => {
10489 const preserveHooks = updatedOptions . preserveHooks ?? false ;
90+ const reusesRequestOptions = updatedOptions === requestOptions ;
91+ const hasExplicitBody = reusesRequestOptions
92+ ? changedState . has ( 'body' ) || changedState . has ( 'json' ) || changedState . has ( 'form' )
93+ : ( Object . hasOwn ( updatedOptions , 'body' ) && updatedOptions . body !== undefined )
94+ || ( Object . hasOwn ( updatedOptions , 'json' ) && updatedOptions . json !== undefined )
95+ || ( Object . hasOwn ( updatedOptions , 'form' ) && updatedOptions . form !== undefined ) ;
96+
97+ if ( hasExplicitBody && ! reusesRequestOptions ) {
98+ options . clearBody ( ) ;
99+ }
105100
106- options . merge ( updatedOptions ) ;
107- options . prefixUrl = '' ;
101+ if ( ! reusesRequestOptions ) {
102+ options . merge ( updatedOptions ) ;
103+ }
108104
109105 if ( updatedOptions . url ) {
110- options . url = updatedOptions . url ;
106+ const nextUrl = reusesRequestOptions
107+ ? options . url as URL
108+ : applyUrlOverride ( options , updatedOptions . url , updatedOptions ) ;
109+
110+ if ( previousUrl ) {
111+ if ( reusesRequestOptions && ! isSameOrigin ( previousUrl , nextUrl ) ) {
112+ options . stripUnchangedCrossOriginState ( previousState ! , changedState , { clearBody : ! hasExplicitBody } ) ;
113+ } else {
114+ options . stripSensitiveHeaders ( previousUrl , nextUrl , updatedOptions ) ;
115+
116+ if ( ! isSameOrigin ( previousUrl , nextUrl ) && ! hasExplicitBody ) {
117+ options . clearBody ( ) ;
118+ }
119+ }
120+ }
111121 }
112122
113123 // Remove any further hooks for that request, because we'll call them anyway.
@@ -118,7 +128,7 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
118128 }
119129
120130 throw new RetryError ( request ) ;
121- } ) ;
131+ } ) ) ;
122132
123133 if ( ! ( is . object ( response ) && is . number ( response . statusCode ) && 'body' in response ) ) {
124134 throw new TypeError ( 'The `afterResponse` hook returned an invalid value' ) ;
@@ -238,6 +248,11 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
238248
239249 const { options} = globalResponse . request ;
240250
251+ if ( responseType === 'text' ) {
252+ const text = decodeUint8Array ( globalResponse . rawBody , options . encoding ) ;
253+ return ( isUtf8Encoding ( options . encoding ) ? text . replace ( / ^ \uFEFF / u, '' ) : text ) as T ;
254+ }
255+
241256 return parseBody ( globalResponse , responseType , options . parseJson , options . encoding ) ;
242257 } ) ( ) ;
243258
0 commit comments