66const { Readable } = require ( 'stream' )
77const http = require ( 'http' )
88const https = require ( 'https' )
9- const net = require ( 'node:net' )
109const zlib = require ( 'zlib' )
1110
1211const { storage } = require ( '../../../../datadog-core' )
1312const log = require ( '../../log' )
14- const { parseUrl } = require ( './url' )
13+ const { isLoopbackHost , parseUrl } = require ( './url' )
1514const docker = require ( './docker' )
1615const { httpAgent, httpsAgent } = require ( './agents' )
1716const {
@@ -27,18 +26,6 @@ const maxActiveBufferSize = 1024 * 1024 * 64
2726
2827let activeBufferSize = 0
2928
30- /**
31- * @param {string } hostname Host as resolved by {@link parseUrl}; IPv6 is unbracketed (`::1`).
32- * @returns {boolean }
33- */
34- function isLoopbackHost ( hostname ) {
35- // The 127.0.0.0/8 block is loopback, but only when the host is an actual IPv4 literal: a
36- // hostname like `127.evil.com` shares the prefix yet resolves anywhere, so net.isIPv4 gates it.
37- return hostname === 'localhost' ||
38- hostname === '::1' ||
39- ( hostname . startsWith ( '127.' ) && net . isIPv4 ( hostname ) )
40- }
41-
4229/**
4330 * @param {Buffer|string|Readable|Array<Buffer|string> } data
4431 * @param {object } options
@@ -110,36 +97,48 @@ function request (data, options, callback) {
11097
11198 /**
11299 * @param {import('node:http').IncomingMessage } res
113- * @param {() => void } finalize
100+ * @param {(error: Error|null, result?: string|null, statusCode?: number,
101+ * headers?: import('node:http').IncomingHttpHeaders) => void} complete
102+ * @param {(error: Error) => void } handleError
114103 */
115- const onResponse = ( res , finalize ) => {
104+ const onResponse = ( res , complete , handleError ) => {
116105 markEndpointReached ( options )
117106
118107 const chunks = [ ]
119108
120109 res . setTimeout ( timeout )
121110
111+ res . once ( 'aborted' , ( ) => {
112+ handleError ( Object . assign ( new Error ( 'Response aborted' ) , { code : 'ECONNRESET' } ) )
113+ } )
114+ res . once ( 'error' , handleError )
115+ res . once ( 'timeout' , ( ) => {
116+ const error = Object . assign ( new Error ( 'Response timed out' ) , { code : 'ETIMEDOUT' } )
117+ res . destroy ( error )
118+ handleError ( error )
119+ } )
120+
122121 res . on ( 'data' , chunk => {
123122 chunks . push ( chunk )
124123 } )
125124
126125 res . once ( 'end' , ( ) => {
127- finalize ( )
128126 const buffer = Buffer . concat ( chunks )
129127
130128 if ( res . statusCode >= 200 && res . statusCode <= 299 ) {
131- const isGzip = res . headers [ 'content-encoding' ] === 'gzip'
129+ const contentEncoding = res . headers [ 'content-encoding' ]
130+ const isGzip = typeof contentEncoding === 'string' && contentEncoding . toLowerCase ( ) === 'gzip'
132131 if ( isGzip ) {
133132 zlib . gunzip ( buffer , ( err , result ) => {
134133 if ( err ) {
135134 log . error ( 'Could not gunzip response: %s' , err . message )
136- callback ( null , '' , res . statusCode , res . headers )
135+ complete ( null , '' , res . statusCode , res . headers )
137136 } else {
138- callback ( null , result . toString ( ) , res . statusCode , res . headers )
137+ complete ( null , result . toString ( ) , res . statusCode , res . headers )
139138 }
140139 } )
141140 } else {
142- callback ( null , buffer . toString ( ) , res . statusCode , res . headers )
141+ complete ( null , buffer . toString ( ) , res . statusCode , res . headers )
143142 }
144143 } else {
145144 let errorMessage = ''
@@ -160,7 +159,7 @@ function request (data, options, callback) {
160159 const error = new log . NoTransmitError ( errorMessage )
161160 error . status = res . statusCode
162161
163- callback ( error , null , res . statusCode , res . headers )
162+ complete ( error , null , res . statusCode , res . headers )
164163 }
165164 } )
166165 }
@@ -179,30 +178,51 @@ function request (data, options, callback) {
179178
180179 legacyStorage . run ( { noop : true } , ( ) => {
181180 let finished = false
181+ let settled = false
182182 const finalize = ( ) => {
183183 if ( finished ) return
184184 finished = true
185185 activeBufferSize -= options . headers [ 'Content-Length' ] ?? 0
186186 }
187187
188- const req = client . request ( options , ( res ) => onResponse ( res , finalize ) )
188+ /**
189+ * @param {Error | null } error
190+ * @param {string | null } [result]
191+ * @param {number } [statusCode]
192+ * @param {import('node:http').IncomingHttpHeaders } [headers]
193+ */
194+ const complete = ( error , result , statusCode , headers ) => {
195+ if ( settled ) return
196+ settled = true
197+ finalize ( )
198+ callback ( error , result , statusCode , headers )
199+ }
189200
190- req . once ( 'close' , finalize )
191- req . once ( 'timeout' , finalize )
201+ /**
202+ * @param {Error } error
203+ */
204+ const handleError = ( error ) => {
205+ if ( settled ) return
192206
193- req . once ( 'error' , error => {
194- finalize ( )
195207 if ( options . retry !== false &&
196208 attemptIndex < getMaxAttempts ( options ) &&
197209 isRetriableNetworkError ( error ) ) {
210+ settled = true
211+ finalize ( )
198212 // Unref so a pending retry never keeps the host process alive past
199213 // its natural exit point; long-running apps still retry because the
200214 // event loop is held open by their own work.
201215 setTimeout ( attempt , getRetryDelay ( options , attemptIndex ) , attemptIndex + 1 ) . unref ?. ( )
202216 } else {
203- callback ( error )
217+ complete ( error )
204218 }
205- } )
219+ }
220+
221+ const req = client . request ( options , ( res ) => onResponse ( res , complete , handleError ) )
222+
223+ req . once ( 'close' , finalize )
224+ req . once ( 'timeout' , finalize )
225+ req . once ( 'error' , handleError )
206226
207227 req . setTimeout ( timeout , ( ) => {
208228 try {
0 commit comments