11import crypto , { createHash } from 'crypto'
22import { createWriteStream , mkdirSync , rmSync } from 'node:fs'
33import { rename } from 'node:fs/promises'
4- import { Agent as HttpAgent } from 'node:http'
5- import { Agent as HttpsAgent , type RequestOptions } from 'node:https'
6- import followRedirects from 'follow-redirects '
4+ import { Agent as HttpAgent , request as httpRequest , type IncomingMessage } from 'node:http'
5+ import { Agent as HttpsAgent , request as httpsRequest , type RequestOptions } from 'node:https'
6+ import { pipeline } from 'node:stream/promises '
77import path from 'path'
88import { debug } from './debug.ts'
99import { Mapper } from './mappers/index.ts'
1010import { Disconnect , Exchange , Filter , FilterForExchange } from './types.ts'
11- const { http : followRedirectsHttp , https : followRedirectsHttps } = followRedirects
1211
1312export function parseAsUTCDate ( val : string ) {
1413 // Treat date-only and minute-level strings as UTC instead of local time.
@@ -456,44 +455,75 @@ async function requestViaFetch(method: string, url: string, options: HttpRequest
456455 }
457456}
458457
459- async function requestViaProxy ( method : string , url : string , options : HttpRequestOptions ) : Promise < HttpResponse > {
460- const requestClient = new URL ( url ) . protocol === 'http:' ? followRedirectsHttp : followRedirectsHttps
461- const preparedRequest = prepareRequest ( method , options )
458+ type DownloadRequestOptions = {
459+ headers : Record < string , string >
460+ timeout : number
461+ }
462462
463- return await new Promise < HttpResponse > ( ( resolve , reject ) => {
464- const request = requestClient
465- . request (
466- url ,
467- {
468- method,
469- agent : getProxyAgent ( url ) ,
470- headers : preparedRequest . headers ,
471- timeout : options . timeout
472- } ,
473- ( response ) => {
474- response . setEncoding ( 'utf8' )
475- let body = ''
476- response . on ( 'error' , reject )
477- response . on ( 'data' , ( chunk ) => ( body += chunk ) )
478- response . on ( 'end' , ( ) => {
479- resolve ( createHttpResponse ( response . statusCode ?? 0 , parseNodeResponseHeaders ( response . headers ) , body ) )
480- } )
481- }
482- )
483- . on ( 'error' , reject )
484- . on ( 'timeout' , ( ) => {
485- reject ( new Error ( 'Request timed out' ) )
486- request . destroy ( )
487- } )
488-
489- if ( preparedRequest . body !== undefined ) {
490- request . write ( preparedRequest . body )
491- }
463+ function sendHttpRequest ( url : string | URL , options : RequestOptions , body ?: string ) : Promise < IncomingMessage > {
464+ const requestUrl = new URL ( url )
465+ const requestClient = requestUrl . protocol === 'http:' ? httpRequest : httpsRequest
466+ const agent = getProxyAgent ( requestUrl ) ?? ( requestUrl . protocol === 'https:' ? httpsAgent : undefined )
467+
468+ return new Promise ( ( resolve , reject ) => {
469+ const request = requestClient ( requestUrl , { ...options , agent } , resolve )
492470
471+ request . once ( 'error' , reject )
472+ request . once ( 'timeout' , ( ) => request . destroy ( new Error ( 'Request timed out' ) ) )
473+ if ( body !== undefined ) {
474+ request . write ( body )
475+ }
493476 request . end ( )
494477 } )
495478}
496479
480+ async function openDownloadResponse ( url : string , options : DownloadRequestOptions ) {
481+ const response = await sendHttpRequest ( url , options )
482+ const statusCode = response . statusCode ?? 0
483+ const location = response . headers . location
484+ if ( location === undefined || statusCode < 300 || statusCode >= 400 ) {
485+ return response
486+ }
487+
488+ response . destroy ( )
489+ const sourceUrl = new URL ( url )
490+ const redirectUrl = new URL ( location , sourceUrl )
491+ const headers : Record < string , string > = { ...options . headers }
492+ if ( sourceUrl . origin !== redirectUrl . origin ) {
493+ delete headers . Authorization
494+ delete headers . authorization
495+ }
496+
497+ return sendHttpRequest ( redirectUrl , {
498+ ...options ,
499+ headers
500+ } )
501+ }
502+
503+ async function readResponseText ( response : IncomingMessage ) {
504+ response . setEncoding ( 'utf8' )
505+ let body = ''
506+ for await ( const chunk of response ) {
507+ body += chunk
508+ }
509+ return body
510+ }
511+
512+ async function requestViaProxy ( method : string , url : string , options : HttpRequestOptions ) : Promise < HttpResponse > {
513+ const preparedRequest = prepareRequest ( method , options )
514+ const response = await sendHttpRequest (
515+ url ,
516+ {
517+ method,
518+ headers : preparedRequest . headers ,
519+ timeout : options . timeout
520+ } ,
521+ preparedRequest . body
522+ )
523+
524+ return createHttpResponse ( response . statusCode ?? 0 , parseNodeResponseHeaders ( response . headers ) , await readResponseText ( response ) )
525+ }
526+
497527async function request ( method : string , url : string , options : HttpRequestOptions = { } ) {
498528 const retrySettings = getRetrySettings ( method , options . retry )
499529
@@ -622,7 +652,12 @@ export function cleanTempFiles() {
622652 tmpFileCleanups . forEach ( ( cleanup ) => cleanup ( ) )
623653}
624654
625- async function _downloadFile ( requestOptions : RequestOptions , url : string , downloadPath : string , appendContentEncodingExtension : boolean ) {
655+ async function _downloadFile (
656+ requestOptions : DownloadRequestOptions ,
657+ url : string ,
658+ downloadPath : string ,
659+ appendContentEncodingExtension : boolean
660+ ) {
626661 // first ensure that directory where we want to download file exists
627662 mkdirSync ( path . dirname ( downloadPath ) , { recursive : true } )
628663
@@ -642,59 +677,28 @@ async function _downloadFile(requestOptions: RequestOptions, url: string, downlo
642677
643678 try {
644679 // based on https://github.com/nodejs/node/issues/28172 - only reliable way to consume response stream and avoiding all the 'gotchas'
645- let responseHeaders : Record < string , string > = { }
646- await new Promise < void > ( ( resolve , reject ) => {
647- const protocol = new URL ( url ) . protocol
648- const requestClient = protocol === 'http:' ? followRedirectsHttp : followRedirectsHttps
649- const agent = getProxyAgent ( url ) ?? ( protocol === 'https:' ? httpsAgent : undefined )
650- const req = requestClient
651- . get ( url , { ...requestOptions , agent } , ( res ) => {
652- const { statusCode } = res
653- if ( statusCode !== 200 ) {
654- // read the error response text and throw it as an HttpError
655- res . setEncoding ( 'utf8' )
656- let body = ''
657- res . on ( 'error' , reject )
658- res . on ( 'data' , ( chunk ) => ( body += chunk ) )
659- res . on ( 'end' , ( ) => {
660- reject ( new HttpError ( statusCode ! , body , url ) )
661- } )
662- } else {
663- responseHeaders = parseNodeResponseHeaders ( res . headers )
664- if ( appendContentEncodingExtension ) {
665- const contentEncoding = asSingleHeaderValue ( res . headers [ 'content-encoding' ] )
666- if ( contentEncoding === 'zstd' ) {
667- finalDownloadPath = `${ downloadPath } .zst`
668- } else if ( contentEncoding === undefined || contentEncoding === 'gzip' ) {
669- finalDownloadPath = `${ downloadPath } .gz`
670- } else {
671- reject ( new Error ( `Unsupported data feed content encoding: ${ contentEncoding } ` ) )
672- return
673- }
674- }
680+ const response = await openDownloadResponse ( url , requestOptions )
681+ const { statusCode } = response
682+ if ( statusCode !== 200 ) {
683+ throw new HttpError ( statusCode ?? 0 , await readResponseText ( response ) , url )
684+ }
675685
676- // consume the response stream by writing it to the file
677- res
678- . on ( 'error' , reject )
679- . on ( 'aborted' , ( ) => reject ( new Error ( 'Request aborted' ) ) )
680- . pipe ( fileWriteStream )
681- . on ( 'error' , reject )
682- . on ( 'finish' , ( ) => {
683- if ( res . complete ) {
684- resolve ( )
685- } else {
686- reject ( new Error ( 'The connection was terminated while the message was still being sent' ) )
687- }
688- } )
689- }
690- } )
691- . on ( 'error' , reject )
692- . on ( 'timeout' , ( ) => {
693- debug ( 'download file request timeout, %s' , url )
694- reject ( new Error ( 'Request timed out' ) )
695- req . destroy ( )
696- } )
697- } )
686+ const responseHeaders = parseNodeResponseHeaders ( response . headers )
687+ if ( appendContentEncodingExtension ) {
688+ const contentEncoding = asSingleHeaderValue ( response . headers [ 'content-encoding' ] )
689+ if ( contentEncoding === 'zstd' ) {
690+ finalDownloadPath = `${ downloadPath } .zst`
691+ } else if ( contentEncoding === undefined || contentEncoding === 'gzip' ) {
692+ finalDownloadPath = `${ downloadPath } .gz`
693+ } else {
694+ throw new Error ( `Unsupported data feed content encoding: ${ contentEncoding } ` )
695+ }
696+ }
697+
698+ await pipeline ( response , fileWriteStream )
699+ if ( ! response . complete ) {
700+ throw new Error ( 'The connection was terminated while the message was still being sent' )
701+ }
698702
699703 // finally when saving from the network to file has succeded, rename tmp file to normal name
700704 // then we're sure that responses is 100% saved and also even if different process was doing the same we're good
0 commit comments