@mswjs/interceptors 0.42.3 → 0.42.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/lib/browser/interceptors/WebSocket/index.js +14 -1
  2. package/lib/browser/interceptors/WebSocket/index.js.map +1 -1
  3. package/lib/browser/interceptors/fetch/web.js +1 -1
  4. package/lib/browser/presets/browser.js +1 -1
  5. package/lib/browser/{web-CdcYFjgm.js → web-DUlFZEtz.js} +58 -3
  6. package/lib/browser/web-DUlFZEtz.js.map +1 -0
  7. package/lib/node/{batch-interceptor-ByEZVih3.js → batch-interceptor-DMOfd-9x.js} +2 -2
  8. package/lib/node/{batch-interceptor-ByEZVih3.js.map → batch-interceptor-DMOfd-9x.js.map} +1 -1
  9. package/lib/node/buffer-utils-B4FUq-l2.js +17 -0
  10. package/lib/node/buffer-utils-B4FUq-l2.js.map +1 -0
  11. package/lib/node/create-request-id-DHo-fRTV.js +14 -0
  12. package/lib/node/create-request-id-DHo-fRTV.js.map +1 -0
  13. package/lib/node/{fetch-utils-Dw1PsmtX.js → fetch-utils-D-_xeRlK.js} +3 -14
  14. package/lib/node/{fetch-utils-Dw1PsmtX.js.map → fetch-utils-D-_xeRlK.js.map} +1 -1
  15. package/lib/node/{has-configurable-global-BU1sT1u4.js → has-configurable-global-CT6-QYdg.js} +2 -2
  16. package/lib/node/{has-configurable-global-BU1sT1u4.js.map → has-configurable-global-CT6-QYdg.js.map} +1 -1
  17. package/lib/node/index.d.ts +2 -243
  18. package/lib/node/index.js +6 -25
  19. package/lib/node/index.js.map +1 -1
  20. package/lib/node/{buffer-utils-BvPY1Tc-.js → interceptor-C8qRPjxG.js} +2 -16
  21. package/lib/node/interceptor-C8qRPjxG.js.map +1 -0
  22. package/lib/node/interceptors/ClientRequest/index.js +3 -3
  23. package/lib/node/interceptors/WebSocket/index.d.ts +2 -0
  24. package/lib/node/interceptors/WebSocket/index.js +635 -0
  25. package/lib/node/interceptors/WebSocket/index.js.map +1 -0
  26. package/lib/node/interceptors/XMLHttpRequest/node.js +5 -5
  27. package/lib/node/interceptors/fetch/node.js +5 -5
  28. package/lib/node/interceptors/http/index.js +2 -2
  29. package/lib/node/interceptors/net/index.d.ts +16 -0
  30. package/lib/node/interceptors/net/index.js +1 -1
  31. package/lib/node/{net-Ca8p1rIe.js → net-Bh16MP4u.js} +82 -92
  32. package/lib/node/net-Bh16MP4u.js.map +1 -0
  33. package/lib/node/patches-registry-DxR5TEc-.js +76 -0
  34. package/lib/node/patches-registry-DxR5TEc-.js.map +1 -0
  35. package/lib/node/remote-http-interceptor.js +4 -4
  36. package/lib/node/resolve-web-socket-url-CSvNPLGi.js +25 -0
  37. package/lib/node/resolve-web-socket-url-CSvNPLGi.js.map +1 -0
  38. package/lib/node/{source-CIgPng7r.js → source-DHVO1vzq.js} +327 -213
  39. package/lib/node/source-DHVO1vzq.js.map +1 -0
  40. package/lib/node/websocket-CC0nB0md.d.ts +285 -0
  41. package/package.json +5 -2
  42. package/src/interceptors/WebSocket/index.ts +4 -1
  43. package/src/interceptors/fetch/web.ts +11 -2
  44. package/src/interceptors/http/http-parser/index.ts +9 -1
  45. package/src/interceptors/http/http-parser.ts +21 -4
  46. package/src/interceptors/http/source.ts +368 -283
  47. package/src/interceptors/net/index.ts +39 -12
  48. package/src/interceptors/net/socket-controller.ts +126 -66
  49. package/src/utils/clone-response.ts +72 -0
  50. package/src/utils/internal-connection.ts +22 -0
  51. package/lib/browser/web-CdcYFjgm.js.map +0 -1
  52. package/lib/node/buffer-utils-BvPY1Tc-.js.map +0 -1
  53. package/lib/node/net-Ca8p1rIe.js.map +0 -1
  54. package/lib/node/source-CIgPng7r.js.map +0 -1
@@ -27,6 +27,7 @@ import {
27
27
  } from '../net/socket-controller'
28
28
  import { unwrapPendingData } from '../net/utils/flush-writes'
29
29
  import { FetchResponse } from '../../utils/fetch-utils'
30
+ import { cloneResponse } from '../../utils/clone-response'
30
31
  import { requestContext } from '../../request-context'
31
32
  import { Interceptor } from '#/src/interceptor'
32
33
 
@@ -67,6 +68,24 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
67
68
  let requestParser: HttpRequestParser | undefined
68
69
  let tunnelUrl: URL | undefined
69
70
  let abortPendingRequest: (() => void) | undefined
71
+ let pendingRequestController: RequestController | undefined
72
+
73
+ // A malformed request loses the boundary for subsequent requests.
74
+ // Preserve the original bytes for the client and server to handle.
75
+ const stopParsingRequests = (error: Error) => {
76
+ httpLogger.verbose('stopping HTTP request parsing: %o', error)
77
+ isHttpConnection = false
78
+
79
+ if (
80
+ pendingRequestController?.readyState === RequestController.PENDING
81
+ ) {
82
+ void pendingRequestController.passthrough()
83
+ } else {
84
+ socketController.decline()
85
+ }
86
+
87
+ requestParser?.free(error)
88
+ }
70
89
 
71
90
  /**
72
91
  * @note Capture the request context of the connection itself.
@@ -93,328 +112,394 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
93
112
  realSocketDestroy(error, callback)
94
113
  }
95
114
 
96
- /**
97
- * @note Only inspect the first sent packet to determine the protocol.
98
- * A single socket cannot be used for different protocols.
99
- */
100
- socket.on('data', (chunk) => {
101
- if (isHttpConnection === false) {
102
- return
115
+ const addRequestDataListener = () => {
116
+ const executeRequestParser = (
117
+ parser: HttpRequestParser,
118
+ chunk: Buffer
119
+ ) => {
120
+ // llhttp pauses permanently at an upgrade boundary. Release the
121
+ // parser after execute returns, outside its native callbacks.
122
+ if (parser.execute(chunk) !== null) {
123
+ socket.removeListener('data', onRequestData)
124
+ parser.free()
125
+ requestParser = undefined
126
+ }
103
127
  }
104
128
 
105
129
  /**
106
- * @note A mocked "CONNECT" request has established a tunnel.
107
- * The data that follows belongs to a new exchange addressed to
108
- * the tunnel target. The parser stopped at the tunnel boundary
109
- * (HTTP upgrade semantics), so tear it down and detect the
110
- * tunneled protocol anew, like on a fresh connection.
130
+ * @note Inspect the first sent packet to determine the protocol,
131
+ * including when entering a mocked "CONNECT" tunnel.
111
132
  */
112
- if (tunnelUrl && requestParser) {
113
- requestParser.free()
114
- requestParser = undefined
115
- isHttpConnection = undefined
133
+ const onRequestData = (chunk: Buffer) => {
134
+ if (isHttpConnection === false) {
135
+ socketController.decline()
136
+ return
137
+ }
116
138
 
117
139
  /**
118
- * @note Retarget the connection to the tunnel authority.
119
- * The exchanges that follow belong to the tunnel target,
120
- * so an unclaimed exchange (HTTP or not) must pass through
121
- * to that target — not to the proxy, which never actually
122
- * established this tunnel — like a real established tunnel
123
- * relays its traffic.
140
+ * @note A mocked "CONNECT" request has established a tunnel.
141
+ * The data that follows belongs to a new exchange addressed to
142
+ * the tunnel target. The previous parser was freed at the upgrade
143
+ * boundary, so detect the tunneled protocol anew.
124
144
  */
125
- socketController.reset({
126
- host: tunnelUrl.hostname,
127
- port: Number(tunnelUrl.port) || 80,
128
- path: null,
129
- })
130
- }
145
+ if (tunnelUrl && !requestParser) {
146
+ isHttpConnection = undefined
131
147
 
132
- if (requestParser) {
133
- requestParser.execute(toBuffer(chunk))
134
- return
135
- }
148
+ /**
149
+ * @note Retarget the connection to the tunnel authority.
150
+ * The exchanges that follow belong to the tunnel target,
151
+ * so an unclaimed exchange (HTTP or not) must pass through
152
+ * to that target — not to the proxy, which never actually
153
+ * established this tunnel — like a real established tunnel
154
+ * relays its traffic.
155
+ */
156
+ socketController.reset({
157
+ host: tunnelUrl.hostname,
158
+ port: Number(tunnelUrl.port) || 80,
159
+ path: null,
160
+ })
161
+ }
136
162
 
137
- const httpMessage = chunk.toString()
138
- const httpMethod = httpMessage.split(' ')[0] || ''
163
+ if (requestParser) {
164
+ executeRequestParser(requestParser, toBuffer(chunk))
165
+ return
166
+ }
139
167
 
140
- // Decline non-HTTP connections so the socket controller can
141
- // pass them through once every subscriber has declined.
142
- if (!METHODS.includes(httpMethod.toUpperCase())) {
143
- isHttpConnection = false
144
- socketController.decline()
145
- return
146
- }
168
+ const httpMessage = chunk.toString()
169
+ const httpMethod = httpMessage.split(' ')[0] || ''
147
170
 
148
- isHttpConnection = true
149
-
150
- const baseUrl =
151
- tunnelUrl ?? connectionOptionsToUrl(connectionOptions, socket)
152
-
153
- httpLogger.verbose('handling http message %o', {
154
- httpMessage,
155
- httpMethod,
156
- baseUrl,
157
- })
158
-
159
- // Get the request initiator from the async context, falling
160
- // back to the context captured at the connection time, then
161
- // to the underlying socket.
162
- const requestContextValue =
163
- requestContext.getStore() ?? connectionRequestContext
164
- const initiator = requestContextValue?.initiator || socket
165
-
166
- requestParser = new HttpRequestParser({
167
- connectionOptions: {
168
- method: httpMethod,
169
- url: baseUrl,
170
- },
171
- /**
172
- * @note The message boundary ends the current exchange.
173
- * Schedule the controller reset so the next write on this
174
- * (kept-alive) socket opens a new exchange and buffers for
175
- * its own verdict instead of following the settled one
176
- * (e.g. leaking a mocked request to the server of a
177
- * previously passed-through exchange).
178
- */
179
- onMessageComplete: () => {
180
- socketController.scheduleReset()
181
- },
182
- onRequest: async (parsedRequest, requestAbortController) => {
183
- const request =
184
- requestContextValue?.transformRequest?.(parsedRequest) ??
185
- parsedRequest
171
+ // Decline non-HTTP connections so the socket controller can
172
+ // pass them through once every subscriber has declined.
173
+ if (!METHODS.includes(httpMethod.toUpperCase())) {
174
+ isHttpConnection = false
175
+ socketController.decline()
176
+ return
177
+ }
186
178
 
187
- /**
188
- * @note A subsequent request arriving on a kept-alive socket
189
- * that has already been handled (passed through or mocked).
190
- * Clients like Undici reuse sockets without emitting the
191
- * "free" event, so reset the controller here, at the HTTP
192
- * message boundary, to handle the new request from the
193
- * pending state again.
194
- */
195
- if (socketController['readyState'] !== SocketController.PENDING) {
196
- socketController.reset()
197
- }
179
+ isHttpConnection = true
198
180
 
199
- const requestId = createRequestId()
200
- const requestLogger = requestContextValue?.logger ?? httpLogger
181
+ const baseUrl =
182
+ tunnelUrl ?? connectionOptionsToUrl(connectionOptions, socket)
201
183
 
202
- httpLogger.verbose('received a parsed HTTP request %o', {
203
- method: request.method,
204
- url: request.url,
205
- })
184
+ httpLogger.verbose('handling http message %o', {
185
+ httpMessage,
186
+ httpMethod,
187
+ baseUrl,
188
+ })
206
189
 
207
- const requestController = new RequestController(
208
- request,
209
- {
210
- respondWith: async (rawResponse) => {
211
- httpLogger.verbose('respondWith() %o', {
212
- status: rawResponse.status,
213
- statusText: rawResponse.statusText,
214
- hasBody: rawResponse.body != null,
215
- })
216
-
217
- /**
218
- * @note The client may destroy the socket (e.g. on request
219
- * abort) moments before a response arrives. A destroyed
220
- * socket cannot be claimed and has no one reading it.
221
- */
222
- if (socket.destroyed) {
223
- return
224
- }
225
-
226
- socketController.claim()
227
-
228
- const response = FetchResponse.from(rawResponse, {
229
- url: request.url,
230
- })
231
-
232
- /**
233
- * @note A successful mocked response to a "CONNECT"
234
- * request establishes a tunnel to the requested authority
235
- * (e.g. "127.0.0.1:80"). The exchange that follows on this
236
- * socket is addressed to that authority, not to the proxy.
237
- */
238
- if (request.method === 'CONNECT' && response.ok) {
239
- tunnelUrl = new URL(`http://${request.url}`)
240
- }
241
-
242
- /**
243
- * @note Clone the response before "respondWith" because it will
244
- * consume its body. This way, we can have a readable response copy
245
- * for the "response" event below.
246
- */
247
- const responseClone = isResponseError(response)
248
- ? null
249
- : response.clone()
250
-
251
- const respond = () => {
252
- return this.respondWith({
253
- socket: socketController[kRawSocket],
254
- request: context.request,
255
- response,
256
- })
257
- }
190
+ // Get the request initiator from the async context, falling
191
+ // back to the context captured at the connection time, then
192
+ // to the underlying socket.
193
+ const requestContextValue =
194
+ requestContext.getStore() ?? connectionRequestContext
195
+ const initiator = requestContextValue?.initiator || socket
196
+
197
+ requestParser = new HttpRequestParser({
198
+ onError: stopParsingRequests,
199
+ connectionOptions: {
200
+ method: httpMethod,
201
+ url: baseUrl,
202
+ },
203
+ /**
204
+ * @note The message boundary ends the current exchange.
205
+ * Schedule the controller reset so the next write on this
206
+ * (kept-alive) socket opens a new exchange and buffers for
207
+ * its own verdict instead of following the settled one
208
+ * (e.g. leaking a mocked request to the server of a
209
+ * previously passed-through exchange).
210
+ */
211
+ onMessageComplete: () => {
212
+ socketController.scheduleReset()
213
+ },
214
+ onRequest: async (parsedRequest, requestAbortController) => {
215
+ const request =
216
+ requestContextValue?.transformRequest?.(parsedRequest) ??
217
+ parsedRequest
218
+
219
+ /**
220
+ * @note A subsequent request arriving on a kept-alive socket
221
+ * that has already been handled (passed through or mocked).
222
+ * Clients like Undici reuse sockets without emitting the
223
+ * "free" event, so reset the controller here, at the HTTP
224
+ * message boundary, to handle the new request from the
225
+ * pending state again.
226
+ */
227
+ if (
228
+ socketController['readyState'] !== SocketController.PENDING
229
+ ) {
230
+ socketController.reset()
231
+ }
258
232
 
259
- if (responseClone) {
260
- await this.emitter.emitAsPromise(
261
- new HttpResponseEvent({
262
- initiator,
263
- requestId,
264
- request: context.request,
265
- response: responseClone,
266
- responseType: 'mock',
267
- })
268
- )
269
- }
233
+ const requestId = createRequestId()
234
+ const requestLogger = requestContextValue?.logger ?? httpLogger
235
+
236
+ httpLogger.verbose('received a parsed HTTP request %o', {
237
+ method: request.method,
238
+ url: request.url,
239
+ })
240
+
241
+ const requestController = new RequestController(
242
+ request,
243
+ {
244
+ respondWith: async (rawResponse) => {
245
+ httpLogger.verbose('respondWith() %o', {
246
+ status: rawResponse.status,
247
+ statusText: rawResponse.statusText,
248
+ hasBody: rawResponse.body != null,
249
+ })
270
250
 
271
- if (socket.connecting) {
272
- // Send a mocked response once the socket connects, just like the real server would.
273
- // This preserves the correct order of events (e.g. connect, then data).
274
- socket.once('connect', respond)
275
- } else {
276
251
  /**
277
- * @note Reused sockets stay connected between requests and will not
278
- * emit "connect" anymore. If that's the case, respond immediately.
252
+ * @note The client may destroy the socket (e.g. on request
253
+ * abort) moments before a response arrives. A destroyed
254
+ * socket cannot be claimed and has no one reading it.
279
255
  */
280
- await respond()
281
- }
282
- },
283
- errorWith: (reason) => {
284
- if (reason instanceof Error) {
285
- socket.destroy(reason)
286
- }
287
- },
288
- passthrough: () => {
289
- const realSocket = socketController.passthrough(
290
- this.#modifyHttpHeaders(context.request)
291
- )
292
-
293
- if (this.emitter.listenerCount('response') > 0) {
294
- httpLogger.verbose(
295
- 'found "response" listener, corking socket reads'
296
- )
256
+ if (socket.destroyed) {
257
+ return
258
+ }
259
+
260
+ socketController.claim()
261
+
262
+ const originalResponse = FetchResponse.from(rawResponse, {
263
+ url: request.url,
264
+ })
265
+ const [response, responseClone] =
266
+ !isResponseError(originalResponse) &&
267
+ this.emitter.listenerCount('response') > 0
268
+ ? cloneResponse(originalResponse)
269
+ : [originalResponse, null]
297
270
 
298
271
  /**
299
- * Suspend the delivery of the original response to the client
300
- * until the "response" event listeners settle. This guarantees
301
- * that the request promise (e.g. `await fetch()`) does not
302
- * resolve before the listeners are done. The real socket keeps
303
- * emitting data for the response parser meanwhile.
272
+ * @note A successful mocked response to a "CONNECT"
273
+ * request establishes a tunnel to the requested authority
274
+ * (e.g. "127.0.0.1:80"). The exchange that follows on this
275
+ * socket is addressed to that authority, not to the proxy.
304
276
  */
305
- socketController.corkReads()
306
-
307
- const responseParser = new HttpResponseParser({
308
- onResponse: async (response) => {
309
- httpLogger.verbose(
310
- 'HTTP response parser parsed: %d %s',
311
- response.status,
312
- response.statusText
313
- )
277
+ if (request.method === 'CONNECT' && response.ok) {
278
+ tunnelUrl = new URL(`http://${request.url}`)
279
+ addRequestDataListener()
280
+ }
281
+
282
+ const respond = () => {
283
+ return this.respondWith({
284
+ socket: socketController[kRawSocket],
285
+ request: context.request,
286
+ response,
287
+ })
288
+ }
289
+
290
+ if (responseClone) {
291
+ await this.emitter.emitAsPromise(
292
+ new HttpResponseEvent({
293
+ initiator,
294
+ requestId,
295
+ request: context.request,
296
+ response: responseClone,
297
+ responseType: 'mock',
298
+ })
299
+ )
300
+ }
301
+
302
+ if (socket.connecting) {
303
+ // Send a mocked response once the socket connects, just like the real server would.
304
+ // This preserves the correct order of events (e.g. connect, then data).
305
+ socket.once('connect', respond)
306
+ } else {
307
+ /**
308
+ * @note Reused sockets stay connected between requests and will not
309
+ * emit "connect" anymore. If that's the case, respond immediately.
310
+ */
311
+ await respond()
312
+ }
313
+ },
314
+ errorWith: (reason) => {
315
+ if (reason instanceof Error) {
316
+ socket.destroy(reason)
317
+ }
318
+ },
319
+ passthrough: () => {
320
+ const realSocket = socketController.passthrough(
321
+ isHttpConnection === false
322
+ ? undefined
323
+ : this.#modifyHttpHeaders(context.request)
324
+ )
314
325
 
315
- if (isResponseError(response)) {
326
+ if (isHttpConnection === false) {
327
+ return
328
+ }
329
+
330
+ if (this.emitter.listenerCount('response') > 0) {
331
+ httpLogger.verbose(
332
+ 'found "response" listener, corking socket reads'
333
+ )
334
+
335
+ /**
336
+ * Suspend the delivery of the original response to the client
337
+ * until the "response" event listeners settle. This guarantees
338
+ * that the request promise (e.g. `await fetch()`) does not
339
+ * resolve before the listeners are done. The real socket keeps
340
+ * emitting data for the response parser meanwhile.
341
+ */
342
+ socketController.corkReads()
343
+
344
+ let responseParserDisposed = false
345
+ let responseComplete = false
346
+ let hasFinalResponse = false
347
+ const responseParser = new HttpResponseParser({
348
+ onError: (error) => {
349
+ disposeResponseParser(error)
350
+ socketController.uncorkReads()
351
+ },
352
+ onMessageComplete: (status) => {
353
+ responseComplete = status >= 200 || status === 101
354
+ },
355
+ onResponse: async (response) => {
356
+ hasFinalResponse =
357
+ response.status >= 200 || response.status === 101
316
358
  httpLogger.verbose(
317
- 'response is an error response, uncorking socket reads...'
359
+ 'HTTP response parser parsed: %d %s',
360
+ response.status,
361
+ response.statusText
318
362
  )
319
363
 
320
- socketController.uncorkReads()
321
- return
322
- }
364
+ if (isResponseError(response)) {
365
+ httpLogger.verbose(
366
+ 'response is an error response, uncorking socket reads...'
367
+ )
323
368
 
324
- FetchResponse.setUrl(request.url, response)
325
-
326
- try {
327
- httpLogger.verbose('emitting "response" event')
328
- await this.emitter.emitAsPromise(
329
- new HttpResponseEvent({
330
- initiator,
331
- requestId,
332
- request: context.request,
333
- response,
334
- responseType: 'original',
335
- })
336
- )
337
- } finally {
338
- httpLogger.verbose('uncorking socket reads')
339
- socketController.uncorkReads()
369
+ socketController.uncorkReads()
370
+ return
371
+ }
340
372
 
341
- /**
342
- * @note Informational responses other than
343
- * "101 Switching Protocols" are followed by a final
344
- * response on the same connection. Keep gating that
345
- * final response on the "response" event listeners.
346
- */
347
- if (
348
- response.status < 200 &&
349
- response.status !== 101
350
- ) {
351
- socketController.corkReads()
373
+ FetchResponse.setUrl(request.url, response)
374
+
375
+ try {
376
+ httpLogger.verbose('emitting "response" event')
377
+ await this.emitter.emitAsPromise(
378
+ new HttpResponseEvent({
379
+ initiator,
380
+ requestId,
381
+ request: context.request,
382
+ response,
383
+ responseType: 'original',
384
+ })
385
+ )
386
+ } finally {
387
+ httpLogger.verbose('uncorking socket reads')
388
+ socketController.uncorkReads()
389
+
390
+ /**
391
+ * @note Informational responses other than
392
+ * "101 Switching Protocols" are followed by a final
393
+ * response on the same connection. Keep gating that
394
+ * final response on the "response" event listeners.
395
+ */
396
+ if (
397
+ !responseParserDisposed &&
398
+ response.status < 200 &&
399
+ response.status !== 101
400
+ ) {
401
+ socketController.corkReads()
402
+ }
352
403
  }
404
+ },
405
+ })
406
+
407
+ const onResponseData = (chunk: Buffer) => {
408
+ responseParser.execute(chunk)
409
+
410
+ // Free only after llhttp returns from its callbacks.
411
+ if (responseComplete) {
412
+ disposeResponseParser()
353
413
  }
354
- },
355
- })
414
+ }
415
+
416
+ const onResponseEnd = () => {
417
+ disposeResponseParser()
356
418
 
357
- realSocket
358
- .on('data', (chunk) => responseParser.execute(chunk))
359
- .on('close', () => responseParser.free())
360
- }
419
+ // Without a final response, release EOF now. A half-open
420
+ // socket cannot close until the client consumes it.
421
+ if (!hasFinalResponse) {
422
+ socketController.uncorkReads()
423
+ }
424
+ }
425
+
426
+ const disposeResponseParser = (error?: Error) => {
427
+ responseParserDisposed = true
428
+ realSocket.removeListener('data', onResponseData)
429
+ realSocket.removeListener('end', onResponseEnd)
430
+ realSocket.removeListener('close', onResponseEnd)
431
+ responseParser.free(error)
432
+ }
433
+
434
+ realSocket
435
+ .on('data', onResponseData)
436
+ .once('end', onResponseEnd)
437
+ .once('close', onResponseEnd)
438
+ }
439
+ },
361
440
  },
362
- },
363
- {
364
- logger: requestLogger,
441
+ {
442
+ logger: requestLogger,
443
+ requestId,
444
+ }
445
+ )
446
+
447
+ invariant(
448
+ socketController['readyState'] === SocketController.PENDING,
449
+ 'CANNOT HANDLE ALREADY HANDLED REQUEST',
450
+ request.method,
451
+ request.url,
452
+ socketController['readyState']
453
+ )
454
+
455
+ /**
456
+ * @note Create a request resolution context.
457
+ * This is so modifications to the "request" in upstream interceptors
458
+ * are correctly picked up by the underlying HTTP interceptor.
459
+ */
460
+ const context: HandleRequestOptions = {
461
+ initiator,
365
462
  requestId,
463
+ request,
464
+ controller: requestController,
465
+ emitter: this.emitter,
466
+ logger: requestLogger,
366
467
  }
367
- )
368
468
 
369
- invariant(
370
- socketController['readyState'] === SocketController.PENDING,
371
- 'CANNOT HANDLE ALREADY HANDLED REQUEST',
372
- request.method,
373
- request.url,
374
- socketController['readyState']
375
- )
469
+ /**
470
+ * @note The client destroying the socket while the request
471
+ * is still pending means the request was aborted (e.g. via
472
+ * `AbortController`). Abort the parsed request so its
473
+ * handling settles and late interactions with the request
474
+ * controller become controlled errors.
475
+ */
476
+ abortPendingRequest = () => {
477
+ if (
478
+ requestController.readyState === RequestController.PENDING
479
+ ) {
480
+ requestAbortController.abort()
481
+ }
482
+ }
376
483
 
377
- /**
378
- * @note Create a request resolution context.
379
- * This is so modifications to the "request" in upstream interceptors
380
- * are correctly picked up by the underlying HTTP interceptor.
381
- */
382
- const context: HandleRequestOptions = {
383
- initiator,
384
- requestId,
385
- request,
386
- controller: requestController,
387
- emitter: this.emitter,
388
- logger: requestLogger,
389
- }
484
+ pendingRequestController = requestController
390
485
 
391
- /**
392
- * @note The client destroying the socket while the request
393
- * is still pending means the request was aborted (e.g. via
394
- * `AbortController`). Abort the parsed request so its
395
- * handling settles and late interactions with the request
396
- * controller become controlled errors.
397
- */
398
- abortPendingRequest = () => {
399
- if (
400
- requestController.readyState === RequestController.PENDING
401
- ) {
402
- requestAbortController.abort()
486
+ try {
487
+ await handleRequest(context)
488
+ } finally {
489
+ pendingRequestController = undefined
490
+ abortPendingRequest = undefined
403
491
  }
404
- }
405
-
406
- try {
407
- await handleRequest(context)
408
- } finally {
409
- abortPendingRequest = undefined
410
- }
411
- },
412
- })
413
-
414
- // Forward the first frame to the parser.
415
- requestParser.execute(toBuffer(chunk))
416
- })
492
+ },
493
+ })
494
+
495
+ // Forward the first frame to the parser.
496
+ executeRequestParser(requestParser, toBuffer(chunk))
497
+ }
498
+
499
+ socket.on('data', onRequestData)
500
+ }
417
501
 
502
+ addRequestDataListener()
418
503
  socket.on('close', () => requestParser?.free())
419
504
  },
420
505
  {