@mswjs/interceptors 0.42.2 → 0.42.4

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 (47) 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/node/{batch-interceptor-ByEZVih3.js → batch-interceptor-DMOfd-9x.js} +2 -2
  4. package/lib/node/{batch-interceptor-ByEZVih3.js.map → batch-interceptor-DMOfd-9x.js.map} +1 -1
  5. package/lib/node/buffer-utils-B4FUq-l2.js +17 -0
  6. package/lib/node/buffer-utils-B4FUq-l2.js.map +1 -0
  7. package/lib/node/create-request-id-DHo-fRTV.js +14 -0
  8. package/lib/node/create-request-id-DHo-fRTV.js.map +1 -0
  9. package/lib/node/{fetch-utils-Dw1PsmtX.js → fetch-utils-Tm5LbwBe.js} +3 -14
  10. package/lib/node/{fetch-utils-Dw1PsmtX.js.map → fetch-utils-Tm5LbwBe.js.map} +1 -1
  11. package/lib/node/{has-configurable-global-IskkJJOL.js → has-configurable-global-CT6-QYdg.js} +2 -2
  12. package/lib/node/{has-configurable-global-IskkJJOL.js.map → has-configurable-global-CT6-QYdg.js.map} +1 -1
  13. package/lib/node/index.d.ts +2 -243
  14. package/lib/node/index.js +6 -25
  15. package/lib/node/index.js.map +1 -1
  16. package/lib/node/{buffer-utils-BvPY1Tc-.js → interceptor-C8qRPjxG.js} +2 -16
  17. package/lib/node/interceptor-C8qRPjxG.js.map +1 -0
  18. package/lib/node/interceptors/ClientRequest/index.js +3 -3
  19. package/lib/node/interceptors/WebSocket/index.d.ts +2 -0
  20. package/lib/node/interceptors/WebSocket/index.js +635 -0
  21. package/lib/node/interceptors/WebSocket/index.js.map +1 -0
  22. package/lib/node/interceptors/XMLHttpRequest/node.js +5 -5
  23. package/lib/node/interceptors/fetch/node.js +5 -5
  24. package/lib/node/interceptors/http/index.js +2 -2
  25. package/lib/node/interceptors/net/index.d.ts +21 -0
  26. package/lib/node/interceptors/net/index.js +1 -1
  27. package/lib/node/{net-9sRKnjIG.js → net-DkiHxhQF.js} +66 -79
  28. package/lib/node/net-DkiHxhQF.js.map +1 -0
  29. package/lib/node/patches-registry-DxR5TEc-.js +76 -0
  30. package/lib/node/patches-registry-DxR5TEc-.js.map +1 -0
  31. package/lib/node/remote-http-interceptor.js +4 -4
  32. package/lib/node/resolve-web-socket-url-CSvNPLGi.js +25 -0
  33. package/lib/node/resolve-web-socket-url-CSvNPLGi.js.map +1 -0
  34. package/lib/node/{source-BUVce4Q1.js → source-BFZ6wg4P.js} +115 -39
  35. package/lib/node/source-BFZ6wg4P.js.map +1 -0
  36. package/lib/node/websocket-CC0nB0md.d.ts +285 -0
  37. package/package.json +5 -2
  38. package/src/interceptors/WebSocket/index.ts +4 -1
  39. package/src/interceptors/http/http-parser/index.ts +9 -1
  40. package/src/interceptors/http/http-parser.ts +34 -5
  41. package/src/interceptors/http/source.ts +82 -3
  42. package/src/interceptors/net/index.ts +39 -12
  43. package/src/interceptors/net/socket-controller.ts +61 -9
  44. package/src/utils/internal-connection.ts +22 -0
  45. package/lib/node/buffer-utils-BvPY1Tc-.js.map +0 -1
  46. package/lib/node/net-9sRKnjIG.js.map +0 -1
  47. package/lib/node/source-BUVce4Q1.js.map +0 -1
@@ -0,0 +1,285 @@
1
+ import { t as Interceptor } from "./interceptor-dtJZ_9QT.js";
2
+ import { TypedEvent } from "rettime";
3
+ //#region src/interceptors/WebSocket/utils/events.d.ts
4
+ declare const kCancelable: unique symbol;
5
+ declare const kDefaultPrevented: unique symbol;
6
+ /**
7
+ * A `MessageEvent` superset that supports event cancellation
8
+ * in Node.js. It's rather non-intrusive so it can be safely
9
+ * used in the browser as well.
10
+ *
11
+ * @see https://github.com/nodejs/node/issues/51767
12
+ */
13
+ declare class CancelableMessageEvent<T = any> extends MessageEvent<T> {
14
+ [kCancelable]: boolean;
15
+ [kDefaultPrevented]: boolean;
16
+ constructor(type: string, init: MessageEventInit<T>);
17
+ get cancelable(): boolean;
18
+ set cancelable(nextCancelable: boolean);
19
+ get defaultPrevented(): boolean;
20
+ set defaultPrevented(nextDefaultPrevented: boolean);
21
+ preventDefault(): void;
22
+ }
23
+ interface CloseEventInit extends EventInit {
24
+ code?: number;
25
+ reason?: string;
26
+ wasClean?: boolean;
27
+ }
28
+ declare class CloseEvent extends Event {
29
+ code: number;
30
+ reason: string;
31
+ wasClean: boolean;
32
+ constructor(type: string, init?: CloseEventInit);
33
+ }
34
+ declare class CancelableCloseEvent extends CloseEvent {
35
+ [kCancelable]: boolean;
36
+ [kDefaultPrevented]: boolean;
37
+ constructor(type: string, init?: CloseEventInit);
38
+ get cancelable(): boolean;
39
+ set cancelable(nextCancelable: boolean);
40
+ get defaultPrevented(): boolean;
41
+ set defaultPrevented(nextDefaultPrevented: boolean);
42
+ preventDefault(): void;
43
+ }
44
+ //#endregion
45
+ //#region src/interceptors/WebSocket/web-socket-transport.d.ts
46
+ type WebSocketData = string | ArrayBuffer | Blob | ArrayBufferView<ArrayBuffer>;
47
+ type WebSocketTransportEventMap = {
48
+ incoming: MessageEvent<WebSocketData>;
49
+ outgoing: MessageEvent<WebSocketData>;
50
+ close: CloseEvent;
51
+ };
52
+ type StrictEventListenerOrEventListenerObject<EventType extends Event> = ((this: WebSocket, event: EventType) => void) | {
53
+ handleEvent(this: WebSocket, event: EventType): void;
54
+ };
55
+ interface WebSocketTransport {
56
+ addEventListener<EventType extends keyof WebSocketTransportEventMap>(event: EventType, listener: StrictEventListenerOrEventListenerObject<WebSocketTransportEventMap[EventType]> | null, options?: boolean | AddEventListenerOptions): void;
57
+ dispatchEvent<EventType extends keyof WebSocketTransportEventMap>(event: WebSocketTransportEventMap[EventType]): boolean;
58
+ /**
59
+ * Send the data from the server to this client.
60
+ */
61
+ send(data: WebSocketData): void;
62
+ /**
63
+ * Close the client connection.
64
+ */
65
+ close(code?: number, reason?: string): void;
66
+ }
67
+ //#endregion
68
+ //#region src/interceptors/WebSocket/web-socket-override.d.ts
69
+ type WebSocketEventListener<EventType extends WebSocketEventMap[keyof WebSocketEventMap] = Event> = (this: WebSocket, event: EventType) => void;
70
+ declare const kPassthroughPromise: unique symbol;
71
+ declare const kOnSend: unique symbol;
72
+ declare const kClose: unique symbol;
73
+ declare class WebSocketOverride extends EventTarget implements WebSocket {
74
+ static readonly CONNECTING = 0;
75
+ static readonly OPEN = 1;
76
+ static readonly CLOSING = 2;
77
+ static readonly CLOSED = 3;
78
+ readonly CONNECTING = 0;
79
+ readonly OPEN = 1;
80
+ readonly CLOSING = 2;
81
+ readonly CLOSED = 3;
82
+ url: string;
83
+ protocol: string;
84
+ extensions: string;
85
+ binaryType: BinaryType;
86
+ readyState: WebSocket['readyState'];
87
+ bufferedAmount: number;
88
+ private _onopen;
89
+ private _onmessage;
90
+ private _onerror;
91
+ private _onclose;
92
+ private [kPassthroughPromise];
93
+ private [kOnSend]?;
94
+ constructor(url: string | URL, protocols?: string | Array<string>);
95
+ set onopen(listener: WebSocketEventListener | null);
96
+ get onopen(): WebSocketEventListener | null;
97
+ set onmessage(listener: WebSocketEventListener<MessageEvent<WebSocketData>> | null);
98
+ get onmessage(): WebSocketEventListener<MessageEvent<WebSocketData>> | null;
99
+ set onerror(listener: WebSocketEventListener | null);
100
+ get onerror(): WebSocketEventListener | null;
101
+ set onclose(listener: WebSocketEventListener<CloseEvent> | null);
102
+ get onclose(): WebSocketEventListener<CloseEvent> | null;
103
+ /**
104
+ * @see https://websockets.spec.whatwg.org/#ref-for-dom-websocket-send%E2%91%A0
105
+ */
106
+ send(data: WebSocketData): void;
107
+ close(code?: number, reason?: string): void;
108
+ private [kClose];
109
+ addEventListener<K extends keyof WebSocketEventMap>(type: K, listener: (this: WebSocket, event: WebSocketEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
110
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
111
+ removeEventListener<K extends keyof WebSocketEventMap>(type: K, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
112
+ }
113
+ //#endregion
114
+ //#region src/interceptors/WebSocket/web-socket-client-connection.d.ts
115
+ declare const kEmitter$1: unique symbol;
116
+ interface WebSocketClientEventMap {
117
+ message: MessageEvent<WebSocketData>;
118
+ close: CloseEvent;
119
+ }
120
+ declare abstract class WebSocketClientConnectionProtocol {
121
+ abstract id: string;
122
+ abstract url: URL;
123
+ abstract send(data: WebSocketData): void;
124
+ abstract close(code?: number, reason?: string): void;
125
+ abstract addEventListener<EventType extends keyof WebSocketClientEventMap>(type: EventType, listener: WebSocketEventListener<WebSocketClientEventMap[EventType]>, options?: AddEventListenerOptions | boolean): void;
126
+ abstract removeEventListener<EventType extends keyof WebSocketClientEventMap>(event: EventType, listener: WebSocketEventListener<WebSocketClientEventMap[EventType]>, options?: EventListenerOptions | boolean): void;
127
+ }
128
+ /**
129
+ * The WebSocket client instance represents an incoming
130
+ * client connection. The user can control the connection,
131
+ * send and receive events.
132
+ */
133
+ declare class WebSocketClientConnection implements WebSocketClientConnectionProtocol {
134
+ readonly socket: WebSocket;
135
+ private readonly transport;
136
+ readonly id: string;
137
+ readonly url: URL;
138
+ private [kEmitter$1];
139
+ constructor(socket: WebSocket, transport: WebSocketTransport);
140
+ /**
141
+ * Listen for the outgoing events from the connected WebSocket client.
142
+ */
143
+ addEventListener<EventType extends keyof WebSocketClientEventMap>(type: EventType, listener: WebSocketEventListener<WebSocketClientEventMap[EventType]>, options?: AddEventListenerOptions | boolean): void;
144
+ /**
145
+ * Removes the listener for the given event.
146
+ */
147
+ removeEventListener<EventType extends keyof WebSocketClientEventMap>(event: EventType, listener: WebSocketEventListener<WebSocketClientEventMap[EventType]>, options?: EventListenerOptions | boolean): void;
148
+ /**
149
+ * Send data to the connected client.
150
+ */
151
+ send(data: WebSocketData): void;
152
+ /**
153
+ * Close the WebSocket connection.
154
+ * @param {number} code A status code (see https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1).
155
+ * @param {string} reason A custom connection close reason.
156
+ */
157
+ close(code?: number, reason?: string): void;
158
+ }
159
+ //#endregion
160
+ //#region src/interceptors/WebSocket/web-socket-class-transport.d.ts
161
+ /**
162
+ * Abstraction over the given mock `WebSocket` instance that allows
163
+ * for controlling that instance (e.g. sending and receiving messages).
164
+ */
165
+ declare class WebSocketClassTransport extends EventTarget implements WebSocketTransport {
166
+ protected readonly socket: WebSocketOverride;
167
+ constructor(socket: WebSocketOverride);
168
+ addEventListener<EventType extends keyof WebSocketTransportEventMap>(type: EventType, callback: StrictEventListenerOrEventListenerObject<WebSocketTransportEventMap[EventType]> | null, options?: boolean | AddEventListenerOptions): void;
169
+ dispatchEvent<EventType extends keyof WebSocketTransportEventMap>(event: WebSocketTransportEventMap[EventType]): boolean;
170
+ send(data: WebSocketData): void;
171
+ close(code: number, reason?: string): void;
172
+ }
173
+ //#endregion
174
+ //#region src/interceptors/WebSocket/web-socket-server-connection.d.ts
175
+ declare const kEmitter: unique symbol;
176
+ declare const kSend: unique symbol;
177
+ interface WebSocketServerEventMap {
178
+ open: Event;
179
+ message: MessageEvent<WebSocketData>;
180
+ error: Event;
181
+ close: CloseEvent;
182
+ }
183
+ declare abstract class WebSocketServerConnectionProtocol {
184
+ abstract connect(): void;
185
+ abstract send(data: WebSocketData): void;
186
+ abstract close(): void;
187
+ abstract addEventListener<EventType extends keyof WebSocketServerEventMap>(event: EventType, listener: WebSocketEventListener<WebSocketServerEventMap[EventType]>, options?: AddEventListenerOptions | boolean): void;
188
+ abstract removeEventListener<EventType extends keyof WebSocketServerEventMap>(event: EventType, listener: WebSocketEventListener<WebSocketServerEventMap[EventType]>, options?: EventListenerOptions | boolean): void;
189
+ }
190
+ /**
191
+ * The WebSocket server instance represents the actual production
192
+ * WebSocket server connection. It's idle by default but you can
193
+ * establish it by calling `server.connect()`.
194
+ */
195
+ declare class WebSocketServerConnection implements WebSocketServerConnectionProtocol {
196
+ private readonly client;
197
+ private readonly transport;
198
+ private readonly createConnection;
199
+ /**
200
+ * A WebSocket instance connected to the original server.
201
+ */
202
+ private realWebSocket?;
203
+ private mockCloseController;
204
+ private realCloseController;
205
+ private [kEmitter];
206
+ constructor(client: WebSocketOverride, transport: WebSocketClassTransport, createConnection: () => WebSocket);
207
+ /**
208
+ * The `WebSocket` instance connected to the original server.
209
+ * Accessing this before calling `server.connect()` will throw.
210
+ */
211
+ get socket(): WebSocket;
212
+ /**
213
+ * Open connection to the original WebSocket server.
214
+ */
215
+ connect(): void;
216
+ /**
217
+ * Listen for the incoming events from the original WebSocket server.
218
+ */
219
+ addEventListener<EventType extends keyof WebSocketServerEventMap>(event: EventType, listener: WebSocketEventListener<WebSocketServerEventMap[EventType]>, options?: AddEventListenerOptions | boolean): void;
220
+ /**
221
+ * Remove the listener for the given event.
222
+ */
223
+ removeEventListener<EventType extends keyof WebSocketServerEventMap>(event: EventType, listener: WebSocketEventListener<WebSocketServerEventMap[EventType]>, options?: EventListenerOptions | boolean): void;
224
+ /**
225
+ * Send data to the original WebSocket server.
226
+ * @example
227
+ * server.send('hello')
228
+ * server.send(new Blob(['hello']))
229
+ * server.send(new TextEncoder().encode('hello'))
230
+ */
231
+ send(data: WebSocketData): void;
232
+ private [kSend];
233
+ /**
234
+ * Close the actual server connection.
235
+ */
236
+ close(): void;
237
+ private handleIncomingMessage;
238
+ private handleMockClose;
239
+ private handleRealClose;
240
+ }
241
+ //#endregion
242
+ //#region src/interceptors/WebSocket/index.d.ts
243
+ /**
244
+ * Intercept the outgoing WebSocket connections created using
245
+ * the global `WebSocket` class.
246
+ */
247
+ declare class WebSocketInterceptor extends Interceptor<WebSocketEventMap$1> {
248
+ static symbol: symbol;
249
+ protected predicate(): boolean;
250
+ protected setup(): void;
251
+ }
252
+ //#endregion
253
+ //#region src/events/websocket.d.ts
254
+ /**
255
+ * The connection information.
256
+ */
257
+ interface WebSocketConnectionInfo {
258
+ /**
259
+ * The protocols supported by the WebSocket client.
260
+ */
261
+ protocols: string | Array<string> | undefined;
262
+ }
263
+ interface WebSocketConnectionEventData {
264
+ /**
265
+ * The incoming WebSocket client connection.
266
+ */
267
+ client: WebSocketClientConnection;
268
+ /**
269
+ * The original WebSocket server connection.
270
+ */
271
+ server: WebSocketServerConnection;
272
+ info: WebSocketConnectionInfo;
273
+ }
274
+ declare class WebSocketConnectionEvent<DataType extends WebSocketConnectionEventData = WebSocketConnectionEventData> extends TypedEvent<DataType, void, 'connection'> {
275
+ client: WebSocketClientConnection;
276
+ server: WebSocketServerConnection;
277
+ info: WebSocketConnectionInfo;
278
+ constructor(data: DataType);
279
+ }
280
+ type WebSocketEventMap$1 = {
281
+ connection: WebSocketConnectionEvent;
282
+ };
283
+ //#endregion
284
+ export { WebSocketServerConnectionProtocol as a, WebSocketClientConnectionProtocol as c, WebSocketTransport as d, CancelableCloseEvent as f, WebSocketServerConnection as i, WebSocketClientEventMap as l, CloseEvent as m, WebSocketEventMap$1 as n, WebSocketServerEventMap as o, CancelableMessageEvent as p, WebSocketInterceptor as r, WebSocketClientConnection as s, WebSocketConnectionEvent as t, WebSocketData as u };
285
+ //# sourceMappingURL=websocket-CC0nB0md.d.ts.map
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@mswjs/interceptors",
4
4
  "description": "Low-level network interception library for Node.js.",
5
- "version": "0.42.2",
5
+ "version": "0.42.4",
6
6
  "main": "./lib/node/index.js",
7
7
  "types": "./lib/node/index.d.ts",
8
8
  "exports": {
@@ -34,7 +34,10 @@
34
34
  },
35
35
  "./fetch/node": "./lib/node/interceptors/fetch/node.js",
36
36
  "./fetch/web": "./lib/browser/interceptors/fetch/web.js",
37
- "./WebSocket": "./lib/browser/interceptors/WebSocket/index.js",
37
+ "./WebSocket": {
38
+ "browser": "./lib/browser/interceptors/WebSocket/index.js",
39
+ "default": "./lib/node/interceptors/WebSocket/index.js"
40
+ },
38
41
  "./RemoteHttpInterceptor": {
39
42
  "browser": null,
40
43
  "default": "./lib/node/remote-http-interceptor.js"
@@ -23,6 +23,7 @@ import { bindEvent } from './utils/bind-event'
23
23
  import { hasConfigurableGlobal } from '../../utils/has-configurable-global'
24
24
  import { patchesRegistry } from '../../utils/patches-registry'
25
25
  import { createLogger } from '../../utils/logger'
26
+ import { runAsInternalConnection } from '../../utils/internal-connection'
26
27
 
27
28
  export {
28
29
  type WebSocketData,
@@ -73,7 +74,9 @@ export class WebSocketInterceptor extends Interceptor<WebSocketEventMap> {
73
74
  })
74
75
 
75
76
  const createConnection = (): WebSocket => {
76
- return Reflect.construct(target, args, newTarget)
77
+ return runAsInternalConnection(() => {
78
+ return Reflect.construct(target, args, newTarget)
79
+ })
77
80
  }
78
81
 
79
82
  // All WebSocket instances are mocked and don't forward
@@ -29,6 +29,7 @@ const KIND_RESPONSE = constants.TYPE.RESPONSE
29
29
  type ParserKind = typeof KIND_REQUEST | typeof KIND_RESPONSE
30
30
 
31
31
  export interface ParserCallbacks<K extends ParserKind> {
32
+ onError?: (error: Error) => void
32
33
  onMessageBegin?: () => number | void
33
34
  onHeadersComplete?: (
34
35
  info: K extends typeof KIND_REQUEST
@@ -305,6 +306,13 @@ export class HttpParser<K extends ParserKind> {
305
306
  const buffer = new Uint8Array(llhttp_memory.buffer)
306
307
  const length = buffer.indexOf(0, errorPointer) - errorPointer
307
308
 
308
- throw new Error(readStringFrom(errorPointer, length))
309
+ const error = new Error(readStringFrom(errorPointer, length))
310
+
311
+ if (this[kCallbacks].onError) {
312
+ this[kCallbacks].onError(error)
313
+ return
314
+ }
315
+
316
+ throw error
309
317
  }
310
318
  }
@@ -4,19 +4,24 @@ import { FetchRequest, FetchResponse } from '../../utils/fetch-utils'
4
4
  import { HttpParser } from './http-parser/index'
5
5
 
6
6
  interface HttpRequestParserOptions {
7
+ onError: (error: Error) => void
7
8
  connectionOptions: {
8
9
  method?: string
9
10
  url: URL
10
11
  }
11
12
  onRequest: (request: Request, abortController: AbortController) => void
13
+ onMessageComplete?: () => void
12
14
  }
13
15
 
14
16
  export class HttpRequestParser extends HttpParser<1> {
15
17
  #requestBodyStream?: Readable
18
+ #upgrade = false
16
19
 
17
20
  constructor(options: HttpRequestParserOptions) {
18
21
  super(1, {
19
- onHeadersComplete: ({ rawHeaders, method, url: path }) => {
22
+ onError: options.onError,
23
+ onHeadersComplete: ({ rawHeaders, method, url: path, upgrade }) => {
24
+ this.#upgrade = upgrade
20
25
  /**
21
26
  * @note When the socket is reused, "connectionOptions" will point
22
27
  * to the "net.connect()" call options that established the connection,
@@ -79,27 +84,44 @@ export class HttpRequestParser extends HttpParser<1> {
79
84
  },
80
85
  onMessageComplete: () => {
81
86
  this.#requestBodyStream?.push(null)
87
+ this.#requestBodyStream = undefined
88
+
89
+ /**
90
+ * @note An upgraded exchange (e.g. "CONNECT", WebSocket) has
91
+ * no message boundary: it takes the connection over, and the
92
+ * bytes that follow belong to it, not to a next exchange.
93
+ */
94
+ if (!this.#upgrade) {
95
+ options.onMessageComplete?.()
96
+ }
82
97
  },
83
98
  })
84
99
  }
85
100
 
86
- public free(): void {
101
+ public free(error?: Error): void {
87
102
  this.destroy()
88
- this.#requestBodyStream?.destroy()
103
+ this.#requestBodyStream?.destroy(error)
89
104
  this.#requestBodyStream = undefined
90
105
  }
91
106
  }
92
107
 
93
108
  export class HttpResponseParser extends HttpParser<2> {
94
109
  #responseBodyStream?: Readable | null
110
+ #status = 0
95
111
 
96
- constructor(options: { onResponse: (response: Response) => void }) {
112
+ constructor(options: {
113
+ onResponse: (response: Response) => void
114
+ onError: (error: Error) => void
115
+ onMessageComplete?: (status: number) => void
116
+ }) {
97
117
  super(2, {
118
+ onError: options.onError,
98
119
  onHeadersComplete: ({
99
120
  rawHeaders,
100
121
  statusCode: status,
101
122
  statusMessage: statusText,
102
123
  }) => {
124
+ this.#status = status
103
125
  const headers = FetchResponse.parseRawHeaders([...rawHeaders])
104
126
 
105
127
  const response = new FetchResponse(
@@ -127,12 +149,19 @@ export class HttpResponseParser extends HttpParser<2> {
127
149
  },
128
150
  onMessageComplete: () => {
129
151
  this.#responseBodyStream?.push(null)
152
+ this.#responseBodyStream = null
153
+ options.onMessageComplete?.(this.#status)
130
154
  },
131
155
  })
132
156
  }
133
157
 
134
- public free(): void {
158
+ public free(error?: Error): void {
135
159
  this.destroy()
160
+
161
+ if (error) {
162
+ this.#responseBodyStream?.destroy(error)
163
+ }
164
+
136
165
  this.#responseBodyStream = null
137
166
  }
138
167
  }
@@ -67,6 +67,24 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
67
67
  let requestParser: HttpRequestParser | undefined
68
68
  let tunnelUrl: URL | undefined
69
69
  let abortPendingRequest: (() => void) | undefined
70
+ let pendingRequestController: RequestController | undefined
71
+
72
+ // A malformed request loses the boundary for subsequent requests.
73
+ // Preserve the original bytes for the client and server to handle.
74
+ const stopParsingRequests = (error: Error) => {
75
+ httpLogger.verbose('stopping HTTP request parsing: %o', error)
76
+ isHttpConnection = false
77
+
78
+ if (
79
+ pendingRequestController?.readyState === RequestController.PENDING
80
+ ) {
81
+ void pendingRequestController.passthrough()
82
+ } else {
83
+ socketController.decline()
84
+ }
85
+
86
+ requestParser?.free(error)
87
+ }
70
88
 
71
89
  /**
72
90
  * @note Capture the request context of the connection itself.
@@ -99,6 +117,7 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
99
117
  */
100
118
  socket.on('data', (chunk) => {
101
119
  if (isHttpConnection === false) {
120
+ socketController.decline()
102
121
  return
103
122
  }
104
123
 
@@ -164,10 +183,22 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
164
183
  const initiator = requestContextValue?.initiator || socket
165
184
 
166
185
  requestParser = new HttpRequestParser({
186
+ onError: stopParsingRequests,
167
187
  connectionOptions: {
168
188
  method: httpMethod,
169
189
  url: baseUrl,
170
190
  },
191
+ /**
192
+ * @note The message boundary ends the current exchange.
193
+ * Schedule the controller reset so the next write on this
194
+ * (kept-alive) socket opens a new exchange and buffers for
195
+ * its own verdict instead of following the settled one
196
+ * (e.g. leaking a mocked request to the server of a
197
+ * previously passed-through exchange).
198
+ */
199
+ onMessageComplete: () => {
200
+ socketController.scheduleReset()
201
+ },
171
202
  onRequest: async (parsedRequest, requestAbortController) => {
172
203
  const request =
173
204
  requestContextValue?.transformRequest?.(parsedRequest) ??
@@ -276,9 +307,15 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
276
307
  },
277
308
  passthrough: () => {
278
309
  const realSocket = socketController.passthrough(
279
- this.#modifyHttpHeaders(context.request)
310
+ isHttpConnection === false
311
+ ? undefined
312
+ : this.#modifyHttpHeaders(context.request)
280
313
  )
281
314
 
315
+ if (isHttpConnection === false) {
316
+ return
317
+ }
318
+
282
319
  if (this.emitter.listenerCount('response') > 0) {
283
320
  httpLogger.verbose(
284
321
  'found "response" listener, corking socket reads'
@@ -293,8 +330,20 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
293
330
  */
294
331
  socketController.corkReads()
295
332
 
333
+ let responseParserDisposed = false
334
+ let responseComplete = false
335
+ let hasFinalResponse = false
296
336
  const responseParser = new HttpResponseParser({
337
+ onError: (error) => {
338
+ disposeResponseParser(error)
339
+ socketController.uncorkReads()
340
+ },
341
+ onMessageComplete: (status) => {
342
+ responseComplete = status >= 200 || status === 101
343
+ },
297
344
  onResponse: async (response) => {
345
+ hasFinalResponse =
346
+ response.status >= 200 || response.status === 101
298
347
  httpLogger.verbose(
299
348
  'HTTP response parser parsed: %d %s',
300
349
  response.status,
@@ -334,6 +383,7 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
334
383
  * final response on the "response" event listeners.
335
384
  */
336
385
  if (
386
+ !responseParserDisposed &&
337
387
  response.status < 200 &&
338
388
  response.status !== 101
339
389
  ) {
@@ -343,9 +393,35 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
343
393
  },
344
394
  })
345
395
 
396
+ const onResponseData = (chunk: Buffer) => {
397
+ responseParser.execute(chunk)
398
+
399
+ // Free only after llhttp returns from its callbacks.
400
+ if (responseComplete) {
401
+ disposeResponseParser()
402
+ }
403
+ }
404
+
405
+ const onResponseClose = () => {
406
+ disposeResponseParser()
407
+
408
+ // Without a response, no response listener will release
409
+ // the buffered EOF/close that rejects the client request.
410
+ if (!hasFinalResponse) {
411
+ socketController.uncorkReads()
412
+ }
413
+ }
414
+
415
+ const disposeResponseParser = (error?: Error) => {
416
+ responseParserDisposed = true
417
+ realSocket.removeListener('data', onResponseData)
418
+ realSocket.removeListener('close', onResponseClose)
419
+ responseParser.free(error)
420
+ }
421
+
346
422
  realSocket
347
- .on('data', (chunk) => responseParser.execute(chunk))
348
- .on('close', () => responseParser.free())
423
+ .on('data', onResponseData)
424
+ .once('close', onResponseClose)
349
425
  }
350
426
  },
351
427
  },
@@ -392,9 +468,12 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
392
468
  }
393
469
  }
394
470
 
471
+ pendingRequestController = requestController
472
+
395
473
  try {
396
474
  await handleRequest(context)
397
475
  } finally {
476
+ pendingRequestController = undefined
398
477
  abortPendingRequest = undefined
399
478
  }
400
479
  },