@@ -95,169 +95,49 @@ const ACCEPT_HEADER_NAME: 'accept' = 'accept';
9595const USER_AGENT_HEADER_NAME : 'user-agent' = 'user-agent' ;
9696const CONTENT_ENCODING_HEADER_NAME : 'content-encoding' = 'content-encoding' ;
9797
98- const makeRequestAsync : FetchFn = async (
99- url : string ,
100- options : IRequestOptions ,
101- redirected : boolean = false
102- ) => {
103- const { body, redirect, noDecode } = options ;
104-
105- return await new Promise (
106- ( resolve : ( result : IWebClientResponse ) => void , reject : ( error : Error ) => void ) => {
107- const parsedUrl : URL = typeof url === 'string' ? new URL ( url ) : url ;
108- const requestFunction : typeof httpRequest | typeof httpsRequest =
109- parsedUrl . protocol === 'https:' ? httpsRequest : httpRequest ;
110-
111- const req : ClientRequest = requestFunction ( url , options , ( response : IncomingMessage ) => {
112- const responseBuffers : ( Buffer | Uint8Array ) [ ] = [ ] ;
113- response . on ( 'data' , ( chunk : string | Buffer | Uint8Array ) => {
114- responseBuffers . push ( Buffer . from ( chunk ) ) ;
115- } ) ;
116- response . on ( 'end' , ( ) => {
117- // Handle retries by calling the method recursively with the redirect URL
118- const statusCode : number | undefined = response . statusCode ;
119- if ( statusCode === 301 || statusCode === 302 ) {
120- switch ( redirect ) {
121- case 'follow' : {
122- const redirectUrl : string | string [ ] | undefined = response . headers . location ;
123- if ( redirectUrl ) {
124- makeRequestAsync ( redirectUrl , options , true ) . then ( resolve ) . catch ( reject ) ;
125- } else {
126- reject (
127- new Error ( `Received status code ${ response . statusCode } with no location header: ${ url } ` )
128- ) ;
129- }
130-
131- break ;
132- }
133- case 'error' :
134- reject ( new Error ( `Received status code ${ response . statusCode } : ${ url } ` ) ) ;
135- return ;
136- }
137- }
138-
139- const responseData : Buffer = Buffer . concat ( responseBuffers ) ;
140- const status : number = response . statusCode || 0 ;
141- const statusText : string | undefined = response . statusMessage ;
142- const headers : Record < string , string | string [ ] | undefined > = response . headers ;
143-
144- let bodyString : string | undefined ;
145- let bodyJson : unknown | undefined ;
146- let decodedBuffer : Buffer | undefined ;
147- const result : IWebClientResponse = {
148- ok : status >= 200 && status < 300 ,
149- status,
150- statusText,
151- redirected,
152- headers,
153- getTextAsync : async ( ) => {
154- if ( bodyString === undefined ) {
155- const buffer : Buffer = await result . getBufferAsync ( ) ;
156- // eslint-disable-next-line require-atomic-updates
157- bodyString = buffer . toString ( ) ;
158- }
159-
160- return bodyString ;
161- } ,
162- getJsonAsync : async < TJson > ( ) => {
163- if ( bodyJson === undefined ) {
164- const text : string = await result . getTextAsync ( ) ;
165- // eslint-disable-next-line require-atomic-updates
166- bodyJson = JSON . parse ( text ) ;
167- }
168-
169- return bodyJson as TJson ;
170- } ,
171- getBufferAsync : async ( ) => {
172- // Determine if the buffer is compressed and decode it if necessary
173- if ( decodedBuffer === undefined ) {
174- let encodings : string | string [ ] | undefined = headers [ CONTENT_ENCODING_HEADER_NAME ] ;
175- if ( ! noDecode && encodings !== undefined ) {
176- const zlib : typeof import ( 'zlib' ) = await import ( 'node:zlib' ) ;
177- if ( ! Array . isArray ( encodings ) ) {
178- encodings = encodings . split ( ',' ) ;
179- }
180-
181- let buffer : Buffer = responseData ;
182- for ( const encoding of encodings ) {
183- let decompressFn : ( buffer : Buffer , callback : import ( 'zlib' ) . CompressCallback ) => void ;
184- switch ( encoding . trim ( ) ) {
185- case DEFLATE_ENCODING : {
186- decompressFn = zlib . inflate . bind ( zlib ) ;
187- break ;
188- }
189- case GZIP_ENCODING : {
190- decompressFn = zlib . gunzip . bind ( zlib ) ;
191- break ;
192- }
193- case BROTLI_ENCODING : {
194- decompressFn = zlib . brotliDecompress . bind ( zlib ) ;
195- break ;
196- }
197- default : {
198- throw new Error ( `Unsupported content-encoding: ${ encodings } ` ) ;
199- }
200- }
201-
202- buffer = await LegacyAdapters . convertCallbackToPromise ( decompressFn , buffer ) ;
203- }
204-
205- // eslint-disable-next-line require-atomic-updates
206- decodedBuffer = buffer ;
207- } else {
208- decodedBuffer = responseData ;
209- }
210- }
211-
212- return decodedBuffer ;
213- }
214- } ;
215- resolve ( result ) ;
216- } ) ;
217- } ) . on ( 'error' , ( error : Error ) => {
218- reject ( error ) ;
219- } ) ;
220-
221- _sendRequestBody ( req , body , reject ) ;
222- }
223- ) ;
224- } ;
225-
22698type StreamFetchFn = (
22799 url : string ,
228100 options : IRequestOptions ,
229101 isRedirect ?: boolean
230102) => Promise < IWebClientStreamResponse > ;
231103
232104/**
233- * Makes an HTTP request that resolves as soon as headers are received, providing the
234- * response body as a readable stream. This avoids buffering the entire response in memory.
105+ * Shared HTTP request core used by both buffer-based and streaming request functions.
106+ * Handles URL parsing, protocol selection, redirect following, body sending, and error handling.
107+ * The `handleResponse` callback is responsible for processing the response and calling
108+ * `resolve`/`reject` to complete the outer promise.
235109 */
236- const makeStreamRequestAsync : StreamFetchFn = async (
110+ function _makeRawRequestAsync < TResponse > (
237111 url : string ,
238112 options : IRequestOptions ,
239- redirected : boolean = false
240- ) => {
113+ redirected : boolean ,
114+ handleResponse : (
115+ response : IncomingMessage ,
116+ redirected : boolean ,
117+ resolve : ( result : TResponse | PromiseLike < TResponse > ) => void ,
118+ reject : ( error : Error ) => void
119+ ) => void ,
120+ selfFn : ( url : string , options : IRequestOptions , isRedirect ?: boolean ) => Promise < TResponse >
121+ ) : Promise < TResponse > {
241122 const { body, redirect } = options ;
242123
243- return await new Promise (
244- ( resolve : ( result : IWebClientStreamResponse ) => void , reject : ( error : Error ) => void ) => {
124+ return new Promise (
125+ ( resolve : ( result : TResponse | PromiseLike < TResponse > ) => void , reject : ( error : Error ) => void ) => {
245126 const parsedUrl : URL = typeof url === 'string' ? new URL ( url ) : url ;
246127 const requestFunction : typeof httpRequest | typeof httpsRequest =
247128 parsedUrl . protocol === 'https:' ? httpsRequest : httpRequest ;
248129
249130 const req : ClientRequest = requestFunction ( url , options , ( response : IncomingMessage ) => {
250131 const statusCode : number | undefined = response . statusCode ;
251132
252- // Handle redirects
253133 if ( statusCode === 301 || statusCode === 302 ) {
254- // Consume/drain the redirect response before following
134+ // Drain the redirect response before following
255135 response . resume ( ) ;
256136 switch ( redirect ) {
257137 case 'follow' : {
258138 const redirectUrl : string | string [ ] | undefined = response . headers . location ;
259- if ( redirectUrl ) {
260- makeStreamRequestAsync ( redirectUrl , options , true ) . then ( resolve ) . catch ( reject ) ;
139+ if ( typeof redirectUrl === 'string' ) {
140+ resolve ( selfFn ( redirectUrl , options , true ) ) ;
261141 } else {
262142 reject (
263143 new Error ( `Received status code ${ response . statusCode } with no location header: ${ url } ` )
@@ -271,40 +151,156 @@ const makeStreamRequestAsync: StreamFetchFn = async (
271151 }
272152 }
273153
154+ handleResponse ( response , redirected , resolve , reject ) ;
155+ } ) . on ( 'error' , ( error : Error ) => {
156+ reject ( error ) ;
157+ } ) ;
158+
159+ const isStream : boolean = ! ! body && typeof ( body as Readable ) . pipe === 'function' ;
160+ if ( isStream ) {
161+ ( body as Readable ) . on ( 'error' , reject ) ;
162+ ( body as Readable ) . pipe ( req ) ;
163+ } else {
164+ req . end ( body as Buffer | undefined ) ;
165+ }
166+ }
167+ ) ;
168+ }
169+
170+ const makeRequestAsync : FetchFn = async (
171+ url : string ,
172+ options : IRequestOptions ,
173+ redirected : boolean = false
174+ ) => {
175+ const { noDecode } = options ;
176+
177+ return _makeRawRequestAsync (
178+ url ,
179+ options ,
180+ redirected ,
181+ (
182+ response : IncomingMessage ,
183+ wasRedirected : boolean ,
184+ resolve : ( result : IWebClientResponse | PromiseLike < IWebClientResponse > ) => void
185+ ) : void => {
186+ const responseBuffers : ( Buffer | Uint8Array ) [ ] = [ ] ;
187+ response . on ( 'data' , ( chunk : string | Buffer | Uint8Array ) => {
188+ responseBuffers . push ( Buffer . from ( chunk ) ) ;
189+ } ) ;
190+ response . on ( 'end' , ( ) => {
191+ const responseData : Buffer = Buffer . concat ( responseBuffers ) ;
274192 const status : number = response . statusCode || 0 ;
275193 const statusText : string | undefined = response . statusMessage ;
276194 const headers : Record < string , string | string [ ] | undefined > = response . headers ;
277195
278- resolve ( {
196+ let bodyString : string | undefined ;
197+ let bodyJson : unknown | undefined ;
198+ let decodedBuffer : Buffer | undefined ;
199+ const result : IWebClientResponse = {
279200 ok : status >= 200 && status < 300 ,
280201 status,
281202 statusText,
282- redirected,
203+ redirected : wasRedirected ,
283204 headers,
284- stream : response
285- } ) ;
286- } ) . on ( 'error' , ( error : Error ) => {
287- reject ( error ) ;
288- } ) ;
205+ getTextAsync : async ( ) => {
206+ if ( bodyString === undefined ) {
207+ const buffer : Buffer = await result . getBufferAsync ( ) ;
208+ // eslint-disable-next-line require-atomic-updates
209+ bodyString = buffer . toString ( ) ;
210+ }
289211
290- _sendRequestBody ( req , body , reject ) ;
291- }
212+ return bodyString ;
213+ } ,
214+ getJsonAsync : async < TJson > ( ) => {
215+ if ( bodyJson === undefined ) {
216+ const text : string = await result . getTextAsync ( ) ;
217+ // eslint-disable-next-line require-atomic-updates
218+ bodyJson = JSON . parse ( text ) ;
219+ }
220+
221+ return bodyJson as TJson ;
222+ } ,
223+ getBufferAsync : async ( ) => {
224+ // Determine if the buffer is compressed and decode it if necessary
225+ if ( decodedBuffer === undefined ) {
226+ let encodings : string | string [ ] | undefined = headers [ CONTENT_ENCODING_HEADER_NAME ] ;
227+ if ( ! noDecode && encodings !== undefined ) {
228+ const zlib : typeof import ( 'zlib' ) = await import ( 'node:zlib' ) ;
229+ if ( ! Array . isArray ( encodings ) ) {
230+ encodings = encodings . split ( ',' ) ;
231+ }
232+
233+ let buffer : Buffer = responseData ;
234+ for ( const encoding of encodings ) {
235+ let decompressFn : ( buffer : Buffer , callback : import ( 'zlib' ) . CompressCallback ) => void ;
236+ switch ( encoding . trim ( ) ) {
237+ case DEFLATE_ENCODING : {
238+ decompressFn = zlib . inflate . bind ( zlib ) ;
239+ break ;
240+ }
241+ case GZIP_ENCODING : {
242+ decompressFn = zlib . gunzip . bind ( zlib ) ;
243+ break ;
244+ }
245+ case BROTLI_ENCODING : {
246+ decompressFn = zlib . brotliDecompress . bind ( zlib ) ;
247+ break ;
248+ }
249+ default : {
250+ throw new Error ( `Unsupported content-encoding: ${ encodings } ` ) ;
251+ }
252+ }
253+
254+ buffer = await LegacyAdapters . convertCallbackToPromise ( decompressFn , buffer ) ;
255+ }
256+
257+ // eslint-disable-next-line require-atomic-updates
258+ decodedBuffer = buffer ;
259+ } else {
260+ decodedBuffer = responseData ;
261+ }
262+ }
263+
264+ return decodedBuffer ;
265+ }
266+ } ;
267+ resolve ( result ) ;
268+ } ) ;
269+ } ,
270+ makeRequestAsync
292271 ) ;
293272} ;
294273
295- function _sendRequestBody (
296- req : ClientRequest ,
297- body : Buffer | Readable | undefined ,
298- reject : ( error : Error ) => void
299- ) : void {
300- const isStream : boolean = ! ! body && typeof ( body as Readable ) . pipe === 'function' ;
301- if ( isStream ) {
302- ( body as Readable ) . on ( 'error' , reject ) ;
303- ( body as Readable ) . pipe ( req ) ;
304- } else {
305- req . end ( body as Buffer | undefined ) ;
306- }
307- }
274+ const makeStreamRequestAsync : StreamFetchFn = async (
275+ url : string ,
276+ options : IRequestOptions ,
277+ redirected : boolean = false
278+ ) => {
279+ return _makeRawRequestAsync (
280+ url ,
281+ options ,
282+ redirected ,
283+ (
284+ response : IncomingMessage ,
285+ wasRedirected : boolean ,
286+ resolve : ( result : IWebClientStreamResponse | PromiseLike < IWebClientStreamResponse > ) => void
287+ ) : void => {
288+ const status : number = response . statusCode || 0 ;
289+ const statusText : string | undefined = response . statusMessage ;
290+ const headers : Record < string , string | string [ ] | undefined > = response . headers ;
291+
292+ resolve ( {
293+ ok : status >= 200 && status < 300 ,
294+ status,
295+ statusText,
296+ redirected : wasRedirected ,
297+ headers,
298+ stream : response
299+ } ) ;
300+ } ,
301+ makeStreamRequestAsync
302+ ) ;
303+ } ;
308304
309305/**
310306 * A helper for issuing HTTP requests.
0 commit comments