@mswjs/interceptors 0.41.8 → 0.42.0
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.
- package/README.md +274 -228
- package/lib/browser/{bufferUtils-BiiO6HZv.mjs → buffer-utils-DJj7YzLG.js} +3 -3
- package/lib/browser/buffer-utils-DJj7YzLG.js.map +1 -0
- package/lib/browser/create-request-id-DlEd4GOA.js +184 -0
- package/lib/browser/create-request-id-DlEd4GOA.js.map +1 -0
- package/lib/browser/{getRawRequest-B1BqgWG6.mjs → fetch-utils-zxA_SD66.js} +123 -52
- package/lib/browser/fetch-utils-zxA_SD66.js.map +1 -0
- package/lib/browser/handle-request-CIOa9O-N.js +167 -0
- package/lib/browser/handle-request-CIOa9O-N.js.map +1 -0
- package/lib/{node/hasConfigurableGlobal-BiTmog1u.mjs → browser/has-configurable-global-Cew-dYqk.js} +5 -7
- package/lib/browser/has-configurable-global-Cew-dYqk.js.map +1 -0
- package/lib/browser/http-CB9rQ7i_.d.ts +105 -0
- package/lib/browser/{index.d.cts → index.d.ts} +33 -38
- package/lib/browser/index.js +130 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/browser/interceptor-dtJZ_9QT.d.ts +50 -0
- package/lib/browser/interceptors/WebSocket/index.d.ts +2 -0
- package/lib/browser/interceptors/WebSocket/{index.mjs → index.js} +40 -31
- package/lib/browser/interceptors/WebSocket/index.js.map +1 -0
- package/lib/browser/interceptors/XMLHttpRequest/web.d.ts +11 -0
- package/lib/browser/interceptors/XMLHttpRequest/web.js +2 -0
- package/lib/browser/interceptors/fetch/web.d.ts +14 -0
- package/lib/browser/interceptors/fetch/web.js +2 -0
- package/lib/browser/presets/browser.d.ts +11 -0
- package/lib/browser/presets/browser.js +12 -0
- package/lib/browser/presets/browser.js.map +1 -0
- package/lib/browser/{resolveWebSocketUrl-C83-x9iE.mjs → resolve-web-socket-url-CSvNPLGi.js} +3 -3
- package/lib/browser/resolve-web-socket-url-CSvNPLGi.js.map +1 -0
- package/lib/browser/web-BjVDkiX0.js +248 -0
- package/lib/browser/web-BjVDkiX0.js.map +1 -0
- package/lib/{node/XMLHttpRequest-DhupxLHX.mjs → browser/web-C2oKsT3Q.js} +328 -256
- package/lib/browser/web-C2oKsT3Q.js.map +1 -0
- package/lib/browser/{interceptors/WebSocket/index.d.cts → websocket-CC0nB0md.d.ts} +41 -33
- package/lib/node/batch-interceptor-ByEZVih3.js +119 -0
- package/lib/node/batch-interceptor-ByEZVih3.js.map +1 -0
- package/lib/node/batch-interceptor-CPjFK2td.d.ts +28 -0
- package/lib/node/buffer-utils-BvPY1Tc-.js +187 -0
- package/lib/node/buffer-utils-BvPY1Tc-.js.map +1 -0
- package/lib/node/fetch-utils-Dw1PsmtX.js +568 -0
- package/lib/node/fetch-utils-Dw1PsmtX.js.map +1 -0
- package/lib/node/has-configurable-global-CJCh_g1I.js +22 -0
- package/lib/node/has-configurable-global-CJCh_g1I.js.map +1 -0
- package/lib/node/http-CB9rQ7i_.d.ts +105 -0
- package/lib/{browser/interceptors/WebSocket/index.d.mts → node/index.d.ts} +120 -60
- package/lib/node/{index.mjs → index.js} +8 -11
- package/lib/node/index.js.map +1 -0
- package/lib/node/interceptor-dtJZ_9QT.d.ts +50 -0
- package/lib/node/interceptors/ClientRequest/index.d.ts +14 -0
- package/lib/node/interceptors/ClientRequest/index.js +67 -0
- package/lib/node/interceptors/ClientRequest/index.js.map +1 -0
- package/lib/node/interceptors/XMLHttpRequest/node.d.ts +12 -0
- package/lib/node/interceptors/XMLHttpRequest/node.js +74 -0
- package/lib/node/interceptors/XMLHttpRequest/node.js.map +1 -0
- package/lib/node/interceptors/fetch/node.d.ts +18 -0
- package/lib/node/interceptors/fetch/node.js +83 -0
- package/lib/node/interceptors/fetch/node.js.map +1 -0
- package/lib/node/interceptors/http/index.d.ts +14 -0
- package/lib/node/interceptors/http/index.js +32 -0
- package/lib/node/interceptors/http/index.js.map +1 -0
- package/lib/node/interceptors/net/index.d.ts +203 -0
- package/lib/node/interceptors/net/index.js +2 -0
- package/lib/node/llhttp/constants.cjs +485 -0
- package/lib/node/llhttp/constants.d.cts +957 -0
- package/lib/node/llhttp/llhttp.wasm +0 -0
- package/lib/node/net-DtMnyEeg.js +1388 -0
- package/lib/node/net-DtMnyEeg.js.map +1 -0
- package/lib/node/presets/node.d.ts +12 -0
- package/lib/node/presets/node.js +17 -0
- package/lib/node/presets/node.js.map +1 -0
- package/lib/node/{RemoteHttpInterceptor.d.cts → remote-http-interceptor.d.ts} +12 -11
- package/lib/node/{RemoteHttpInterceptor.mjs → remote-http-interceptor.js} +38 -33
- package/lib/node/remote-http-interceptor.js.map +1 -0
- package/lib/node/source-lP0yyEtA.js +1990 -0
- package/lib/node/source-lP0yyEtA.js.map +1 -0
- package/package.json +79 -118
- package/src/batch-interceptor.test.ts +475 -0
- package/src/batch-interceptor.ts +244 -0
- package/src/{createRequestId.test.ts → create-request-id.test.ts} +1 -2
- package/src/disposable.ts +13 -0
- package/src/events/http.ts +92 -0
- package/src/events/websocket.ts +47 -0
- package/src/glossary.ts +1 -27
- package/src/index.ts +18 -10
- package/src/interceptor.test.ts +279 -0
- package/src/interceptor.ts +139 -0
- package/src/interceptors/ClientRequest/index.test.ts +21 -39
- package/src/interceptors/ClientRequest/index.ts +65 -180
- package/src/interceptors/ClientRequest/utils/{recordRawHeaders.test.ts → record-raw-headers.test.ts} +1 -2
- package/src/interceptors/ClientRequest/utils/{recordRawHeaders.ts → record-raw-headers.ts} +29 -2
- package/src/interceptors/WebSocket/index.ts +34 -51
- package/src/interceptors/WebSocket/utils/{bindEvent.test.ts → bind-event.test.ts} +1 -2
- package/src/interceptors/WebSocket/utils/events.test.ts +0 -1
- package/src/interceptors/WebSocket/{WebSocketClassTransport.ts → web-socket-class-transport.ts} +3 -3
- package/src/interceptors/WebSocket/{WebSocketClientConnection.ts → web-socket-client-connection.ts} +4 -4
- package/src/interceptors/WebSocket/{WebSocketOverride.ts → web-socket-override.ts} +6 -7
- package/src/interceptors/WebSocket/{WebSocketServerConnection.ts → web-socket-server-connection.ts} +4 -4
- package/src/interceptors/WebSocket/{WebSocketTransport.ts → web-socket-transport.ts} +1 -1
- package/src/interceptors/XMLHttpRequest/node.ts +104 -0
- package/src/interceptors/XMLHttpRequest/polyfills/{ProgressEventPolyfill.ts → progress-event-polyfill.ts} +1 -1
- package/src/interceptors/XMLHttpRequest/utils/{concateArrayBuffer.test.ts → concat-array-buffer.test.ts} +1 -2
- package/src/interceptors/XMLHttpRequest/utils/{concatArrayBuffer.ts → concat-array-buffer.ts} +1 -1
- package/src/interceptors/XMLHttpRequest/utils/{createEvent.test.ts → create-event.test.ts} +3 -4
- package/src/interceptors/XMLHttpRequest/utils/{createEvent.ts → create-event.ts} +2 -2
- package/src/interceptors/XMLHttpRequest/utils/{createResponse.ts → create-response.ts} +1 -1
- package/src/interceptors/XMLHttpRequest/utils/{getBodyByteLength.test.ts → get-body-byte-length.test.ts} +1 -2
- package/src/interceptors/XMLHttpRequest/web.ts +34 -0
- package/src/interceptors/XMLHttpRequest/{XMLHttpRequestController.ts → xml-http-request-controller.ts} +363 -204
- package/src/interceptors/XMLHttpRequest/{XMLHttpRequestProxy.ts → xml-http-request-proxy.ts} +28 -25
- package/src/interceptors/fetch/node.ts +103 -0
- package/src/interceptors/fetch/utils/{followRedirect.ts → follow-redirect.ts} +1 -1
- package/src/interceptors/fetch/web.ts +217 -0
- package/src/interceptors/http/forward-events.test.ts +86 -0
- package/src/interceptors/http/forward-events.ts +139 -0
- package/src/interceptors/http/http-parser/index.ts +310 -0
- package/src/interceptors/http/http-parser/llhttp/constants.cjs +485 -0
- package/src/interceptors/http/http-parser/llhttp/constants.d.cts +957 -0
- package/src/interceptors/http/http-parser/llhttp/llhttp.wasm +0 -0
- package/src/interceptors/http/http-parser.ts +138 -0
- package/src/interceptors/http/index.ts +34 -0
- package/src/interceptors/http/source.ts +688 -0
- package/src/interceptors/net/index.ts +368 -0
- package/src/interceptors/net/socket-controller.ts +1476 -0
- package/src/interceptors/net/utils/address-info.ts +54 -0
- package/src/interceptors/net/utils/connection-options-to-url.ts +56 -0
- package/src/interceptors/net/utils/flush-writes.ts +57 -0
- package/src/interceptors/net/utils/get-tls-connect-options.ts +23 -0
- package/src/interceptors/net/utils/normalize-net-connect-args.test.ts +233 -0
- package/src/interceptors/net/utils/normalize-net-connect-args.ts +121 -0
- package/src/interceptors/net/utils/normalize-tls-connect-args.ts +50 -0
- package/src/presets/browser.ts +4 -4
- package/src/presets/node.ts +4 -4
- package/src/{RemoteHttpInterceptor.ts → remote-http-interceptor.ts} +55 -42
- package/src/request-context.ts +40 -0
- package/src/{RequestController.test.ts → request-controller.test.ts} +37 -3
- package/src/{RequestController.ts → request-controller.ts} +33 -11
- package/src/utils/{bufferUtils.test.ts → buffer-utils.test.ts} +1 -2
- package/src/utils/buffer-utils.ts +37 -0
- package/src/utils/{cloneObject.test.ts → clone-object.test.ts} +1 -2
- package/src/utils/{cloneObject.ts → clone-object.ts} +8 -8
- package/src/utils/{createProxy.test.ts → create-proxy.test.ts} +6 -4
- package/src/utils/{createProxy.ts → create-proxy.ts} +1 -1
- package/src/utils/{fetchUtils.test.ts → fetch-utils.test.ts} +1 -1
- package/src/utils/{fetchUtils.ts → fetch-utils.ts} +28 -3
- package/src/utils/{findPropertySource.test.ts → find-property-source.test.ts} +1 -2
- package/src/utils/{getCleanUrl.test.ts → get-clean-url.test.ts} +1 -2
- package/src/utils/{getUrlByRequestOptions.test.ts → get-url-by-request-options.test.ts} +4 -8
- package/src/utils/{getUrlByRequestOptions.ts → get-url-by-request-options.ts} +16 -16
- package/src/utils/{getValueBySymbol.test.ts → get-value-by-symbol.test.ts} +1 -2
- package/src/utils/{handleRequest.ts → handle-request.ts} +66 -47
- package/src/utils/{hasConfigurableGlobal.test.ts → has-configurable-global.test.ts} +1 -2
- package/src/utils/{hasConfigurableGlobal.ts → has-configurable-global.ts} +2 -4
- package/src/utils/{isObject.test.ts → is-object.test.ts} +1 -2
- package/src/utils/logger.test.ts +154 -0
- package/src/utils/logger.ts +154 -0
- package/src/utils/{parseJson.test.ts → parse-json.test.ts} +1 -2
- package/src/utils/{patchesRegistry.test.ts → patches-registry.test.ts} +1 -1
- package/src/utils/{patchesRegistry.ts → patches-registry.ts} +1 -1
- package/src/utils/{responseUtils.ts → response-utils.ts} +25 -2
- package/ClientRequest/package.json +0 -11
- package/RemoteHttpInterceptor/package.json +0 -11
- package/WebSocket/package.json +0 -12
- package/XMLHttpRequest/package.json +0 -12
- package/fetch/package.json +0 -12
- package/lib/browser/Interceptor-Deczogc8.d.cts +0 -65
- package/lib/browser/Interceptor-gqKgs-aF.d.mts +0 -65
- package/lib/browser/XMLHttpRequest-B11I9TBx.cjs +0 -751
- package/lib/browser/XMLHttpRequest-B11I9TBx.cjs.map +0 -1
- package/lib/browser/XMLHttpRequest-DHEkSoRN.mjs +0 -746
- package/lib/browser/XMLHttpRequest-DHEkSoRN.mjs.map +0 -1
- package/lib/browser/bufferUtils-BiiO6HZv.mjs.map +0 -1
- package/lib/browser/bufferUtils-Uc0eRItL.cjs +0 -38
- package/lib/browser/bufferUtils-Uc0eRItL.cjs.map +0 -1
- package/lib/browser/createRequestId-DOf8Ktjs.cjs +0 -205
- package/lib/browser/createRequestId-DOf8Ktjs.cjs.map +0 -1
- package/lib/browser/createRequestId-DYCsFHOi.mjs +0 -170
- package/lib/browser/createRequestId-DYCsFHOi.mjs.map +0 -1
- package/lib/browser/fetch-BJwfdses.mjs +0 -236
- package/lib/browser/fetch-BJwfdses.mjs.map +0 -1
- package/lib/browser/fetch-M8iXkZ3L.cjs +0 -241
- package/lib/browser/fetch-M8iXkZ3L.cjs.map +0 -1
- package/lib/browser/getRawRequest-B1BqgWG6.mjs.map +0 -1
- package/lib/browser/getRawRequest-DdfaiPVH.cjs +0 -376
- package/lib/browser/getRawRequest-DdfaiPVH.cjs.map +0 -1
- package/lib/browser/glossary-1lhZx6Aw.d.cts +0 -69
- package/lib/browser/glossary-nkHWcwvY.d.mts +0 -69
- package/lib/browser/handleRequest-1Yqu_ho_.mjs +0 -178
- package/lib/browser/handleRequest-1Yqu_ho_.mjs.map +0 -1
- package/lib/browser/handleRequest-Cz4_wmQ9.cjs +0 -189
- package/lib/browser/handleRequest-Cz4_wmQ9.cjs.map +0 -1
- package/lib/browser/hasConfigurableGlobal-C8zq1MCg.mjs +0 -109
- package/lib/browser/hasConfigurableGlobal-C8zq1MCg.mjs.map +0 -1
- package/lib/browser/hasConfigurableGlobal-CS7adEvV.cjs +0 -126
- package/lib/browser/hasConfigurableGlobal-CS7adEvV.cjs.map +0 -1
- package/lib/browser/index.cjs +0 -70
- package/lib/browser/index.cjs.map +0 -1
- package/lib/browser/index.d.mts +0 -125
- package/lib/browser/index.mjs +0 -56
- package/lib/browser/index.mjs.map +0 -1
- package/lib/browser/interceptors/WebSocket/index.cjs +0 -618
- package/lib/browser/interceptors/WebSocket/index.cjs.map +0 -1
- package/lib/browser/interceptors/WebSocket/index.mjs.map +0 -1
- package/lib/browser/interceptors/XMLHttpRequest/index.cjs +0 -8
- package/lib/browser/interceptors/XMLHttpRequest/index.d.cts +0 -15
- package/lib/browser/interceptors/XMLHttpRequest/index.d.mts +0 -15
- package/lib/browser/interceptors/XMLHttpRequest/index.mjs +0 -8
- package/lib/browser/interceptors/fetch/index.cjs +0 -7
- package/lib/browser/interceptors/fetch/index.d.cts +0 -13
- package/lib/browser/interceptors/fetch/index.d.mts +0 -13
- package/lib/browser/interceptors/fetch/index.mjs +0 -7
- package/lib/browser/presets/browser.cjs +0 -18
- package/lib/browser/presets/browser.cjs.map +0 -1
- package/lib/browser/presets/browser.d.cts +0 -12
- package/lib/browser/presets/browser.d.mts +0 -14
- package/lib/browser/presets/browser.mjs +0 -18
- package/lib/browser/presets/browser.mjs.map +0 -1
- package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs +0 -31
- package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs.map +0 -1
- package/lib/browser/resolveWebSocketUrl-C83-x9iE.mjs.map +0 -1
- package/lib/node/BatchInterceptor-DdMNfSUY.mjs +0 -44
- package/lib/node/BatchInterceptor-DdMNfSUY.mjs.map +0 -1
- package/lib/node/BatchInterceptor-KKF8Q47I.d.cts +0 -26
- package/lib/node/BatchInterceptor-Z1lwK23r.cjs +0 -49
- package/lib/node/BatchInterceptor-Z1lwK23r.cjs.map +0 -1
- package/lib/node/BatchInterceptor-rSr-NH_H.d.mts +0 -26
- package/lib/node/ClientRequest-DFs7FPNi.cjs +0 -1058
- package/lib/node/ClientRequest-DFs7FPNi.cjs.map +0 -1
- package/lib/node/ClientRequest-dRpI9v1o.mjs +0 -1049
- package/lib/node/ClientRequest-dRpI9v1o.mjs.map +0 -1
- package/lib/node/Interceptor-BsZ21ue0.d.cts +0 -132
- package/lib/node/Interceptor-soD-WpQO.d.mts +0 -132
- package/lib/node/RemoteHttpInterceptor.cjs +0 -155
- package/lib/node/RemoteHttpInterceptor.cjs.map +0 -1
- package/lib/node/RemoteHttpInterceptor.d.mts +0 -39
- package/lib/node/RemoteHttpInterceptor.mjs.map +0 -1
- package/lib/node/XMLHttpRequest-DhupxLHX.mjs.map +0 -1
- package/lib/node/XMLHttpRequest-Dyi6ahGz.cjs +0 -752
- package/lib/node/XMLHttpRequest-Dyi6ahGz.cjs.map +0 -1
- package/lib/node/bufferUtils-DxPxwff_.mjs +0 -20
- package/lib/node/bufferUtils-DxPxwff_.mjs.map +0 -1
- package/lib/node/bufferUtils-S5_-2eN4.cjs +0 -38
- package/lib/node/bufferUtils-S5_-2eN4.cjs.map +0 -1
- package/lib/node/chunk-CbDLau6x.cjs +0 -34
- package/lib/node/fetch-BqMcbuYR.mjs +0 -252
- package/lib/node/fetch-BqMcbuYR.mjs.map +0 -1
- package/lib/node/fetch-aQJu1Gf_.cjs +0 -259
- package/lib/node/fetch-aQJu1Gf_.cjs.map +0 -1
- package/lib/node/fetchUtils-BKJ1XmiO.mjs +0 -480
- package/lib/node/fetchUtils-BKJ1XmiO.mjs.map +0 -1
- package/lib/node/fetchUtils-umV5xXBy.cjs +0 -546
- package/lib/node/fetchUtils-umV5xXBy.cjs.map +0 -1
- package/lib/node/getRawRequest-B6znU6il.cjs +0 -36
- package/lib/node/getRawRequest-B6znU6il.cjs.map +0 -1
- package/lib/node/getRawRequest-C2-1urzA.mjs +0 -24
- package/lib/node/getRawRequest-C2-1urzA.mjs.map +0 -1
- package/lib/node/handleRequest-DCLzePtS.mjs +0 -190
- package/lib/node/handleRequest-DCLzePtS.mjs.map +0 -1
- package/lib/node/handleRequest-DVOthWJo.cjs +0 -220
- package/lib/node/handleRequest-DVOthWJo.cjs.map +0 -1
- package/lib/node/hasConfigurableGlobal-BiTmog1u.mjs.map +0 -1
- package/lib/node/hasConfigurableGlobal-MjY06_Ok.cjs +0 -108
- package/lib/node/hasConfigurableGlobal-MjY06_Ok.cjs.map +0 -1
- package/lib/node/index-BJ5E5aW-.d.cts +0 -113
- package/lib/node/index-DfoF45ih.d.mts +0 -113
- package/lib/node/index.cjs +0 -53
- package/lib/node/index.cjs.map +0 -1
- package/lib/node/index.d.cts +0 -104
- package/lib/node/index.d.mts +0 -104
- package/lib/node/index.mjs.map +0 -1
- package/lib/node/interceptors/ClientRequest/index.cjs +0 -6
- package/lib/node/interceptors/ClientRequest/index.d.cts +0 -2
- package/lib/node/interceptors/ClientRequest/index.d.mts +0 -3
- package/lib/node/interceptors/ClientRequest/index.mjs +0 -6
- package/lib/node/interceptors/XMLHttpRequest/index.cjs +0 -7
- package/lib/node/interceptors/XMLHttpRequest/index.d.cts +0 -14
- package/lib/node/interceptors/XMLHttpRequest/index.d.mts +0 -14
- package/lib/node/interceptors/XMLHttpRequest/index.mjs +0 -7
- package/lib/node/interceptors/fetch/index.cjs +0 -6
- package/lib/node/interceptors/fetch/index.d.cts +0 -12
- package/lib/node/interceptors/fetch/index.d.mts +0 -12
- package/lib/node/interceptors/fetch/index.mjs +0 -6
- package/lib/node/node-DIKcnzhK.cjs +0 -39
- package/lib/node/node-DIKcnzhK.cjs.map +0 -1
- package/lib/node/node-lsdNwZEW.mjs +0 -27
- package/lib/node/node-lsdNwZEW.mjs.map +0 -1
- package/lib/node/presets/node.cjs +0 -23
- package/lib/node/presets/node.cjs.map +0 -1
- package/lib/node/presets/node.d.cts +0 -13
- package/lib/node/presets/node.d.mts +0 -15
- package/lib/node/presets/node.mjs +0 -23
- package/lib/node/presets/node.mjs.map +0 -1
- package/lib/node/utils/node/index.cjs +0 -4
- package/lib/node/utils/node/index.d.cts +0 -16
- package/lib/node/utils/node/index.d.mts +0 -16
- package/lib/node/utils/node/index.mjs +0 -3
- package/presets/browser/package.json +0 -5
- package/presets/node/package.json +0 -11
- package/src/BatchInterceptor.test.ts +0 -255
- package/src/BatchInterceptor.ts +0 -95
- package/src/Interceptor.test.ts +0 -205
- package/src/Interceptor.ts +0 -249
- package/src/getRawRequest.ts +0 -21
- package/src/interceptors/ClientRequest/MockHttpSocket.ts +0 -724
- package/src/interceptors/ClientRequest/agents.ts +0 -110
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +0 -54
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +0 -45
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +0 -427
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +0 -268
- package/src/interceptors/ClientRequest/utils/parserUtils.ts +0 -48
- package/src/interceptors/Socket/MockSocket.test.ts +0 -264
- package/src/interceptors/Socket/MockSocket.ts +0 -58
- package/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts +0 -26
- package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts +0 -52
- package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts +0 -33
- package/src/interceptors/XMLHttpRequest/index.ts +0 -40
- package/src/interceptors/fetch/index.ts +0 -203
- package/src/utils/bufferUtils.ts +0 -22
- package/src/utils/canParseUrl.ts +0 -13
- package/src/utils/emitAsync.ts +0 -25
- package/src/utils/nextTick.ts +0 -11
- package/src/utils/node/index.ts +0 -39
- /package/src/{createRequestId.ts → create-request-id.ts} +0 -0
- /package/src/{InterceptorError.ts → interceptor-error.ts} +0 -0
- /package/src/interceptors/WebSocket/utils/{bindEvent.ts → bind-event.ts} +0 -0
- /package/src/interceptors/XMLHttpRequest/polyfills/{EventPolyfill.ts → event-polyfill.ts} +0 -0
- /package/src/interceptors/XMLHttpRequest/utils/{getBodyByteLength.ts → get-body-byte-length.ts} +0 -0
- /package/src/interceptors/XMLHttpRequest/utils/{isDomParserSupportedType.ts → is-dom-parser-supported-type.ts} +0 -0
- /package/src/interceptors/fetch/utils/{createNetworkError.ts → create-network-error.ts} +0 -0
- /package/src/utils/{findPropertySource.ts → find-property-source.ts} +0 -0
- /package/src/utils/{getCleanUrl.ts → get-clean-url.ts} +0 -0
- /package/src/utils/{getValueBySymbol.ts → get-value-by-symbol.ts} +0 -0
- /package/src/utils/{isNodeLikeError.ts → is-node-like-error.ts} +0 -0
- /package/src/utils/{isObject.ts → is-object.ts} +0 -0
- /package/src/utils/{isPropertyAccessible.ts → is-property-accessible.ts} +0 -0
- /package/src/utils/{parseJson.ts → parse-json.ts} +0 -0
- /package/src/utils/{resolveWebSocketUrl.ts → resolve-web-socket-url.ts} +0 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import net from 'node:net'
|
|
2
|
+
import {
|
|
3
|
+
METHODS,
|
|
4
|
+
STATUS_CODES,
|
|
5
|
+
ServerResponse,
|
|
6
|
+
IncomingMessage,
|
|
7
|
+
} from 'node:http'
|
|
8
|
+
import { invariant } from 'outvariant'
|
|
9
|
+
import { HttpResponseEvent, type HttpRequestEventMap } from '../../events/http'
|
|
10
|
+
import { RequestController } from '../../request-controller'
|
|
11
|
+
import {
|
|
12
|
+
getRawFetchHeaders,
|
|
13
|
+
recordRawFetchHeaders,
|
|
14
|
+
} from '../ClientRequest/utils/record-raw-headers'
|
|
15
|
+
import { SocketInterceptor } from '../net'
|
|
16
|
+
import { connectionOptionsToUrl } from '../net/utils/connection-options-to-url'
|
|
17
|
+
import { toBuffer } from '../../utils/buffer-utils'
|
|
18
|
+
import { createRequestId } from '../../create-request-id'
|
|
19
|
+
import { HttpRequestParser, HttpResponseParser } from './http-parser'
|
|
20
|
+
import { handleRequest, HandleRequestOptions } from '../../utils/handle-request'
|
|
21
|
+
import { isResponseError, kErrorResponse } from '../../utils/response-utils'
|
|
22
|
+
import { createLogger } from '../../utils/logger'
|
|
23
|
+
import {
|
|
24
|
+
kRawSocket,
|
|
25
|
+
SocketController,
|
|
26
|
+
type FlushPendingDataFunction,
|
|
27
|
+
} from '../net/socket-controller'
|
|
28
|
+
import { unwrapPendingData } from '../net/utils/flush-writes'
|
|
29
|
+
import { FetchResponse } from '../../utils/fetch-utils'
|
|
30
|
+
import { requestContext } from '../../request-context'
|
|
31
|
+
import { Interceptor } from '#/src/interceptor'
|
|
32
|
+
|
|
33
|
+
const httpLogger = createLogger('http-request')
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Interceptor for HTTP requests in Node.js.
|
|
37
|
+
* Routes socket connections through an HTTP parser.
|
|
38
|
+
*/
|
|
39
|
+
export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
40
|
+
static symbol = Symbol.for('node-http-request-source')
|
|
41
|
+
|
|
42
|
+
protected predicate(): boolean {
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected setup(): void {
|
|
47
|
+
const socketInterceptor = Interceptor.singleton(SocketInterceptor)
|
|
48
|
+
socketInterceptor.apply(this)
|
|
49
|
+
this.subscriptions.push(() => {
|
|
50
|
+
socketInterceptor.dispose(this)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @note Record the raw values provided to Headers set/append
|
|
55
|
+
* in order to support "IncomingMessage.prototype.rawHeaders".
|
|
56
|
+
* This is meant for the headers in mocked responses.
|
|
57
|
+
*/
|
|
58
|
+
this.subscriptions.push(recordRawFetchHeaders())
|
|
59
|
+
|
|
60
|
+
const controller = new AbortController()
|
|
61
|
+
this.subscriptions.push(() => controller.abort())
|
|
62
|
+
|
|
63
|
+
socketInterceptor.on(
|
|
64
|
+
'connection',
|
|
65
|
+
({ connectionOptions, socket, controller: socketController }) => {
|
|
66
|
+
let isHttpConnection: boolean | undefined
|
|
67
|
+
let requestParser: HttpRequestParser | undefined
|
|
68
|
+
let tunnelUrl: URL | undefined
|
|
69
|
+
let abortPendingRequest: (() => void) | undefined
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @note Capture the request context of the connection itself.
|
|
73
|
+
* The socket is created synchronously within the request async
|
|
74
|
+
* context (e.g. inside the patched `http.request()`), but the
|
|
75
|
+
* first data may reach the socket from a foreign context (e.g.
|
|
76
|
+
* a form-data stream piped into the request), where sampling
|
|
77
|
+
* the request context yields nothing.
|
|
78
|
+
*/
|
|
79
|
+
const connectionRequestContext = requestContext.getStore()
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @note The client destroys the socket synchronously (e.g. Undici
|
|
83
|
+
* on request abort) but the socket teardown events ("error",
|
|
84
|
+
* "close") are emitted asynchronously, after the consumer has
|
|
85
|
+
* already observed the rejected request promise. Hook into the
|
|
86
|
+
* destroy itself so the pending request is aborted before any
|
|
87
|
+
* of its listeners can resume.
|
|
88
|
+
*/
|
|
89
|
+
const rawSocket = socketController[kRawSocket]
|
|
90
|
+
const realSocketDestroy = rawSocket._destroy.bind(rawSocket)
|
|
91
|
+
rawSocket._destroy = (error, callback) => {
|
|
92
|
+
abortPendingRequest?.()
|
|
93
|
+
realSocketDestroy(error, callback)
|
|
94
|
+
}
|
|
95
|
+
|
|
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
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
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.
|
|
111
|
+
*/
|
|
112
|
+
if (tunnelUrl && requestParser) {
|
|
113
|
+
requestParser.free()
|
|
114
|
+
requestParser = undefined
|
|
115
|
+
isHttpConnection = undefined
|
|
116
|
+
socketController.reset()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (requestParser) {
|
|
120
|
+
requestParser.execute(toBuffer(chunk))
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const httpMessage = chunk.toString()
|
|
125
|
+
const httpMethod = httpMessage.split(' ')[0] || ''
|
|
126
|
+
|
|
127
|
+
// Ignore non-HTTP packets sent via this socket.
|
|
128
|
+
if (!METHODS.includes(httpMethod.toUpperCase())) {
|
|
129
|
+
isHttpConnection = false
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
isHttpConnection = true
|
|
134
|
+
|
|
135
|
+
const baseUrl =
|
|
136
|
+
tunnelUrl ?? connectionOptionsToUrl(connectionOptions, socket)
|
|
137
|
+
|
|
138
|
+
httpLogger.verbose('handling http message %o', {
|
|
139
|
+
httpMessage,
|
|
140
|
+
httpMethod,
|
|
141
|
+
baseUrl,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Get the request initiator from the async context, falling
|
|
145
|
+
// back to the context captured at the connection time, then
|
|
146
|
+
// to the underlying socket.
|
|
147
|
+
const requestContextValue =
|
|
148
|
+
requestContext.getStore() ?? connectionRequestContext
|
|
149
|
+
const initiator = requestContextValue?.initiator || socket
|
|
150
|
+
|
|
151
|
+
requestParser = new HttpRequestParser({
|
|
152
|
+
connectionOptions: {
|
|
153
|
+
method: httpMethod,
|
|
154
|
+
url: baseUrl,
|
|
155
|
+
},
|
|
156
|
+
onRequest: async (parsedRequest, requestAbortController) => {
|
|
157
|
+
const request =
|
|
158
|
+
requestContextValue?.transformRequest?.(parsedRequest) ??
|
|
159
|
+
parsedRequest
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @note A subsequent request arriving on a kept-alive socket
|
|
163
|
+
* that has already been handled (passed through or mocked).
|
|
164
|
+
* Clients like Undici reuse sockets without emitting the
|
|
165
|
+
* "free" event, so reset the controller here, at the HTTP
|
|
166
|
+
* message boundary, to handle the new request from the
|
|
167
|
+
* pending state again.
|
|
168
|
+
*/
|
|
169
|
+
if (socketController['readyState'] !== SocketController.PENDING) {
|
|
170
|
+
socketController.reset()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const requestId = createRequestId()
|
|
174
|
+
const requestLogger = requestContextValue?.logger ?? httpLogger
|
|
175
|
+
|
|
176
|
+
httpLogger.verbose('received a parsed HTTP request %o', {
|
|
177
|
+
method: request.method,
|
|
178
|
+
url: request.url,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const requestController = new RequestController(
|
|
182
|
+
request,
|
|
183
|
+
{
|
|
184
|
+
respondWith: async (rawResponse) => {
|
|
185
|
+
httpLogger.verbose('respondWith() %o', {
|
|
186
|
+
status: rawResponse.status,
|
|
187
|
+
statusText: rawResponse.statusText,
|
|
188
|
+
hasBody: rawResponse.body != null,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @note The client may destroy the socket (e.g. on request
|
|
193
|
+
* abort) moments before a response arrives. A destroyed
|
|
194
|
+
* socket cannot be claimed and has no one reading it.
|
|
195
|
+
*/
|
|
196
|
+
if (socket.destroyed) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
socketController.claim()
|
|
201
|
+
|
|
202
|
+
const response = FetchResponse.from(rawResponse, {
|
|
203
|
+
url: request.url,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @note A successful mocked response to a "CONNECT"
|
|
208
|
+
* request establishes a tunnel to the requested authority
|
|
209
|
+
* (e.g. "127.0.0.1:80"). The exchange that follows on this
|
|
210
|
+
* socket is addressed to that authority, not to the proxy.
|
|
211
|
+
*/
|
|
212
|
+
if (request.method === 'CONNECT' && response.ok) {
|
|
213
|
+
tunnelUrl = new URL(`http://${request.url}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @note Clone the response before "respondWith" because it will
|
|
218
|
+
* consume its body. This way, we can have a readable response copy
|
|
219
|
+
* for the "response" event below.
|
|
220
|
+
*/
|
|
221
|
+
const responseClone = isResponseError(response)
|
|
222
|
+
? null
|
|
223
|
+
: response.clone()
|
|
224
|
+
|
|
225
|
+
const respond = () => {
|
|
226
|
+
return this.respondWith({
|
|
227
|
+
socket: socketController[kRawSocket],
|
|
228
|
+
request: context.request,
|
|
229
|
+
response,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (responseClone) {
|
|
234
|
+
await this.emitter.emitAsPromise(
|
|
235
|
+
new HttpResponseEvent({
|
|
236
|
+
initiator,
|
|
237
|
+
requestId,
|
|
238
|
+
request: context.request,
|
|
239
|
+
response: responseClone,
|
|
240
|
+
responseType: 'mock',
|
|
241
|
+
})
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (socket.connecting) {
|
|
246
|
+
// Send a mocked response once the socket connects, just like the real server would.
|
|
247
|
+
// This preserves the correct order of events (e.g. connect, then data).
|
|
248
|
+
socket.once('connect', respond)
|
|
249
|
+
} else {
|
|
250
|
+
/**
|
|
251
|
+
* @note Reused sockets stay connected between requests and will not
|
|
252
|
+
* emit "connect" anymore. If that's the case, respond immediately.
|
|
253
|
+
*/
|
|
254
|
+
await respond()
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
errorWith: (reason) => {
|
|
258
|
+
if (reason instanceof Error) {
|
|
259
|
+
socket.destroy(reason)
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
passthrough: () => {
|
|
263
|
+
const realSocket = socketController.passthrough(
|
|
264
|
+
this.#modifyHttpHeaders(context.request)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if (this.emitter.listenerCount('response') > 0) {
|
|
268
|
+
httpLogger.verbose(
|
|
269
|
+
'found "response" listener, corking socket reads'
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Suspend the delivery of the original response to the client
|
|
274
|
+
* until the "response" event listeners settle. This guarantees
|
|
275
|
+
* that the request promise (e.g. `await fetch()`) does not
|
|
276
|
+
* resolve before the listeners are done. The real socket keeps
|
|
277
|
+
* emitting data for the response parser meanwhile.
|
|
278
|
+
*/
|
|
279
|
+
socketController.corkReads()
|
|
280
|
+
|
|
281
|
+
const responseParser = new HttpResponseParser({
|
|
282
|
+
onResponse: async (response) => {
|
|
283
|
+
httpLogger.verbose(
|
|
284
|
+
'HTTP response parser parsed: %d %s',
|
|
285
|
+
response.status,
|
|
286
|
+
response.statusText
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if (isResponseError(response)) {
|
|
290
|
+
httpLogger.verbose(
|
|
291
|
+
'response is an error response, uncorking socket reads...'
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
socketController.uncorkReads()
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
FetchResponse.setUrl(request.url, response)
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
httpLogger.verbose('emitting "response" event')
|
|
302
|
+
await this.emitter.emitAsPromise(
|
|
303
|
+
new HttpResponseEvent({
|
|
304
|
+
initiator,
|
|
305
|
+
requestId,
|
|
306
|
+
request: context.request,
|
|
307
|
+
response,
|
|
308
|
+
responseType: 'original',
|
|
309
|
+
})
|
|
310
|
+
)
|
|
311
|
+
} finally {
|
|
312
|
+
httpLogger.verbose('uncorking socket reads')
|
|
313
|
+
socketController.uncorkReads()
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @note Informational responses other than
|
|
317
|
+
* "101 Switching Protocols" are followed by a final
|
|
318
|
+
* response on the same connection. Keep gating that
|
|
319
|
+
* final response on the "response" event listeners.
|
|
320
|
+
*/
|
|
321
|
+
if (
|
|
322
|
+
response.status < 200 &&
|
|
323
|
+
response.status !== 101
|
|
324
|
+
) {
|
|
325
|
+
socketController.corkReads()
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
realSocket
|
|
332
|
+
.on('data', (chunk) => responseParser.execute(chunk))
|
|
333
|
+
.on('close', () => responseParser.free())
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
logger: requestLogger,
|
|
339
|
+
requestId,
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
invariant(
|
|
344
|
+
socketController['readyState'] === SocketController.PENDING,
|
|
345
|
+
'CANNOT HANDLE ALREADY HANDLED REQUEST',
|
|
346
|
+
request.method,
|
|
347
|
+
request.url,
|
|
348
|
+
socketController['readyState']
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @note Create a request resolution context.
|
|
353
|
+
* This is so modifications to the "request" in upstream interceptors
|
|
354
|
+
* are correctly picked up by the underlying HTTP interceptor.
|
|
355
|
+
*/
|
|
356
|
+
const context: HandleRequestOptions = {
|
|
357
|
+
initiator,
|
|
358
|
+
requestId,
|
|
359
|
+
request,
|
|
360
|
+
controller: requestController,
|
|
361
|
+
emitter: this.emitter,
|
|
362
|
+
logger: requestLogger,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @note The client destroying the socket while the request
|
|
367
|
+
* is still pending means the request was aborted (e.g. via
|
|
368
|
+
* `AbortController`). Abort the parsed request so its
|
|
369
|
+
* handling settles and late interactions with the request
|
|
370
|
+
* controller become controlled errors.
|
|
371
|
+
*/
|
|
372
|
+
abortPendingRequest = () => {
|
|
373
|
+
if (
|
|
374
|
+
requestController.readyState === RequestController.PENDING
|
|
375
|
+
) {
|
|
376
|
+
requestAbortController.abort()
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
await handleRequest(context)
|
|
382
|
+
} finally {
|
|
383
|
+
abortPendingRequest = undefined
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// Forward the first frame to the parser.
|
|
389
|
+
requestParser.execute(toBuffer(chunk))
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
socket.on('close', () => requestParser?.free())
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
signal: controller.signal,
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private async respondWith(args: {
|
|
401
|
+
socket: net.Socket
|
|
402
|
+
request: Request
|
|
403
|
+
response: Response
|
|
404
|
+
}): Promise<void> {
|
|
405
|
+
const { socket, request, response } = args
|
|
406
|
+
|
|
407
|
+
if (socket.destroyed) {
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (isResponseError(response)) {
|
|
412
|
+
/**
|
|
413
|
+
* @note Reference the error response on the socket error so the
|
|
414
|
+
* client-side interceptors (e.g. fetch) can surface it to the
|
|
415
|
+
* consumer as the reason behind the failed request. Keep the
|
|
416
|
+
* reference non-enumerable so the error remains observably
|
|
417
|
+
* identical for the clients that expose it as-is.
|
|
418
|
+
*/
|
|
419
|
+
socket.destroy(
|
|
420
|
+
Object.defineProperty(new TypeError('Network error'), kErrorResponse, {
|
|
421
|
+
value: response,
|
|
422
|
+
enumerable: false,
|
|
423
|
+
})
|
|
424
|
+
)
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
invariant(
|
|
429
|
+
!socket.connecting,
|
|
430
|
+
'Failed to mock a response for "%s %s": socket has not connected',
|
|
431
|
+
request.method,
|
|
432
|
+
request.url
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Use native server response handling in Node.js.
|
|
437
|
+
* @see https://github.com/nodejs/node/blob/13eb80f3b718452213e0fc449702aefbbfe4110f/lib/_http_server.js#L202
|
|
438
|
+
*/
|
|
439
|
+
const incomingMessage = new IncomingMessage(socket)
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* @note Describe the request method so the response body is
|
|
443
|
+
* handled appropriately (e.g. "HEAD" responses must not write
|
|
444
|
+
* a body). The HTTP version is deliberately left unset: with it,
|
|
445
|
+
* `ServerResponse` frames bodies of unknown length as chunked,
|
|
446
|
+
* polluting the mocked response headers with "Transfer-Encoding"
|
|
447
|
+
* the mock never specified.
|
|
448
|
+
*/
|
|
449
|
+
incomingMessage.method = request.method
|
|
450
|
+
|
|
451
|
+
const serverResponse = new ServerResponse(incomingMessage)
|
|
452
|
+
|
|
453
|
+
const responseSocket = new net.Socket()
|
|
454
|
+
|
|
455
|
+
responseSocket._writeGeneric = (writev, data, encoding, callback) => {
|
|
456
|
+
unwrapPendingData(data, (chunk, encoding) => {
|
|
457
|
+
socket.push(toBuffer(chunk), encoding)
|
|
458
|
+
})
|
|
459
|
+
callback?.()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
responseSocket._destroy = (
|
|
463
|
+
error: Error | null,
|
|
464
|
+
callback: (error: Error | null) => void
|
|
465
|
+
) => {
|
|
466
|
+
/**
|
|
467
|
+
* Only destroy the socket on stream errors.
|
|
468
|
+
* On a clean end, the socket is already signaled via `socket.push(null)`
|
|
469
|
+
* in the main response flow. Destroying it here prematurely would prevent
|
|
470
|
+
* the client from processing the response (e.g. calling `response.destroy()`).
|
|
471
|
+
* @see https://github.com/mswjs/interceptors/issues/738
|
|
472
|
+
*/
|
|
473
|
+
if (error) {
|
|
474
|
+
socket.destroy()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
callback(null)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
responseSocket.on('drain', () => serverResponse.emit('drain'))
|
|
481
|
+
serverResponse.assignSocket(responseSocket)
|
|
482
|
+
|
|
483
|
+
serverResponse.removeHeader('connection')
|
|
484
|
+
serverResponse.removeHeader('date')
|
|
485
|
+
|
|
486
|
+
const rawResponseHeaders = getRawFetchHeaders(response.headers)
|
|
487
|
+
serverResponse.writeHead(
|
|
488
|
+
response.status,
|
|
489
|
+
response.statusText || STATUS_CODES[response.status],
|
|
490
|
+
rawResponseHeaders
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* @note Override the socket's `_destroy` before writing the response body.
|
|
495
|
+
* The underlying TCP handle (from `socket.connect()`) makes `_destroy` async
|
|
496
|
+
* (`_handle.close()` callback), which delays the 'error' event. Since the real
|
|
497
|
+
* TCP connection is irrelevant for mocked responses, take the synchronous path
|
|
498
|
+
* so that user-initiated `response.destroy(error)` emits the error promptly.
|
|
499
|
+
* This must happen before `serverResponse.end()` because the HTTP parser may
|
|
500
|
+
* fire the 'response' event synchronously during `socket.push()`.
|
|
501
|
+
*/
|
|
502
|
+
socket._destroy = function (
|
|
503
|
+
error: Error | null,
|
|
504
|
+
callback: (error: Error | null) => void
|
|
505
|
+
) {
|
|
506
|
+
if (error) {
|
|
507
|
+
/**
|
|
508
|
+
* Emit the error event as a microtask instead of relying on the default
|
|
509
|
+
* `process.nextTick(emitErrorNT)` from `callback(error)`. This is necessary
|
|
510
|
+
* because `respondWith` runs inside a microtask (from `await reader.read()`).
|
|
511
|
+
* A resolved promise continuation (from toWebResponse) is queued as
|
|
512
|
+
* another microtask during the same phase. Since microtasks are drained before
|
|
513
|
+
* nextTick, the test's `await` would resolve before the error event fires.
|
|
514
|
+
* Using `queueMicrotask` ensures the error event is emitted within the current
|
|
515
|
+
* microtask phase, before other queued microtasks.
|
|
516
|
+
*/
|
|
517
|
+
queueMicrotask(() => this.emit('error', error))
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
callback(null)
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @note `net.Socket` is constructed with `emitClose: false`, so Node's
|
|
524
|
+
* stream destroy machinery does not emit `'close'` automatically; the
|
|
525
|
+
* stock `net.Socket._destroy` only emits it via `_handle.close()`.
|
|
526
|
+
* Since this override replaces `_destroy`, emit `'close'` here so the
|
|
527
|
+
* mocked socket completes its lifecycle (otherwise consumers waiting
|
|
528
|
+
* on `'close'`, like `http.ClientRequest`, hang).
|
|
529
|
+
*/
|
|
530
|
+
process.nextTick(() => this.emit('close', error != null))
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (response.body) {
|
|
534
|
+
const reader = response.body.getReader()
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
while (true) {
|
|
538
|
+
const { done, value } = await reader.read()
|
|
539
|
+
|
|
540
|
+
if (done) {
|
|
541
|
+
serverResponse.end()
|
|
542
|
+
break
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!serverResponse.write(value)) {
|
|
546
|
+
await new Promise<void>((resolve) => {
|
|
547
|
+
serverResponse.once('drain', resolve)
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
/**
|
|
553
|
+
* @note Delay the socket destruction to allow the event loop
|
|
554
|
+
* to flush already-pushed response data (headers + body chunks)
|
|
555
|
+
* through the HTTP parser. Without this, the socket is destroyed
|
|
556
|
+
* on the same tick as `socket.push(data)` and the client never
|
|
557
|
+
* reads the response.
|
|
558
|
+
*/
|
|
559
|
+
await new Promise<void>((resolve) => process.nextTick(resolve))
|
|
560
|
+
socket.destroy()
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
serverResponse.end()
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* @note Self-delimiting responses (chunked, explicit "Content-Length",
|
|
569
|
+
* or bodiless by definition) must NOT signal the end-of-stream.
|
|
570
|
+
* The client parser completes them from their framing alone, and
|
|
571
|
+
* ending the socket would kill the kept-alive connection that
|
|
572
|
+
* agents pool and reuse for subsequent requests.
|
|
573
|
+
*/
|
|
574
|
+
const isSelfDelimitingResponse =
|
|
575
|
+
request.method === 'HEAD' ||
|
|
576
|
+
response.headers.has('content-length') ||
|
|
577
|
+
response.headers.has('transfer-encoding') ||
|
|
578
|
+
!FetchResponse.isResponseWithBody(response.status)
|
|
579
|
+
|
|
580
|
+
if (request.method !== 'CONNECT' && !isSelfDelimitingResponse) {
|
|
581
|
+
/**
|
|
582
|
+
* @note Defer the end-of-stream signal so the HTTP parser has a chance
|
|
583
|
+
* to process already-pushed response data and fire the 'response' event
|
|
584
|
+
* before the socket is ended. Without this, the parser marks the response
|
|
585
|
+
* as "complete" before the client can interact with it (e.g. `response.destroy()`).
|
|
586
|
+
*/
|
|
587
|
+
await new Promise<void>((resolve) => process.nextTick(resolve))
|
|
588
|
+
socket.push(null)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#modifyHttpHeaders(request: Request): FlushPendingDataFunction {
|
|
593
|
+
const transformRequestMessage = (
|
|
594
|
+
httpMessage: string | Buffer,
|
|
595
|
+
encoding?: BufferEncoding | 'buffer'
|
|
596
|
+
): string | Buffer => {
|
|
597
|
+
/**
|
|
598
|
+
* @note Socket can write a buffer (e.g. uploaded file) even before
|
|
599
|
+
* it writes the HTTP message. Bypass those cases.
|
|
600
|
+
*/
|
|
601
|
+
if (encoding === 'buffer') {
|
|
602
|
+
return httpMessage
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const parts = httpMessage.toString(encoding).split('\r\n')
|
|
606
|
+
const headersEndIndex = parts.findIndex((field) => field === '')
|
|
607
|
+
const httpMessageHeaderPairs = parts.slice(1, headersEndIndex)
|
|
608
|
+
|
|
609
|
+
// Extract raw [name, value] tuples from the wire format so they
|
|
610
|
+
// can be compared against the request's raw fetch headers.
|
|
611
|
+
const httpMessageRawHeaders = httpMessageHeaderPairs.map(
|
|
612
|
+
(line): [string, string] => {
|
|
613
|
+
const separatorIndex = line.indexOf(': ')
|
|
614
|
+
return [line.slice(0, separatorIndex), line.slice(separatorIndex + 2)]
|
|
615
|
+
}
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
const requestRawHeaders = getRawFetchHeaders(request.headers)
|
|
619
|
+
|
|
620
|
+
// If the raw headers from the outgoing HTTP message and the request
|
|
621
|
+
// headers are identical, send the message as-is to avoid the cost
|
|
622
|
+
// (and side effects) of reserializing the headers block.
|
|
623
|
+
const headersUnchanged =
|
|
624
|
+
httpMessageRawHeaders.length === requestRawHeaders.length &&
|
|
625
|
+
httpMessageRawHeaders.every((tuple, index) => {
|
|
626
|
+
const requestTuple = requestRawHeaders[index]
|
|
627
|
+
return tuple[0] === requestTuple[0] && tuple[1] === requestTuple[1]
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
if (headersUnchanged) {
|
|
631
|
+
return httpMessage
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const httpMessageHeaders = FetchResponse.parseRawHeaders(
|
|
635
|
+
httpMessageHeaderPairs.flatMap((header) => header.split(': '))
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
const visitedHeaders = new Set<string>()
|
|
639
|
+
|
|
640
|
+
for (const [headerName] of requestRawHeaders) {
|
|
641
|
+
const normalizedHeaderName = headerName.toLowerCase()
|
|
642
|
+
|
|
643
|
+
if (visitedHeaders.has(normalizedHeaderName)) {
|
|
644
|
+
continue
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
visitedHeaders.add(normalizedHeaderName)
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* @note Forbidden Fetch headers (e.g. Host, Origin, Connection)
|
|
651
|
+
* are stripped from `request.headers` but remain in the raw
|
|
652
|
+
* headers list. Skip them so the original values from the
|
|
653
|
+
* outgoing HTTP message are preserved.
|
|
654
|
+
*/
|
|
655
|
+
const headerValue = request.headers.get(headerName)
|
|
656
|
+
if (headerValue === null) {
|
|
657
|
+
continue
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Use the merged value from Headers to correctly handle
|
|
661
|
+
// appended headers (e.g. "1, 2" instead of just "2").
|
|
662
|
+
httpMessageHeaders.set(headerName, headerValue)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
visitedHeaders.clear()
|
|
666
|
+
|
|
667
|
+
const httpMessageHeadersString = Array.from(httpMessageHeaders)
|
|
668
|
+
.map(([name, value]) => `${name}: ${value}`)
|
|
669
|
+
.join('\r\n')
|
|
670
|
+
parts.splice(1, headersEndIndex - 1, httpMessageHeadersString)
|
|
671
|
+
|
|
672
|
+
return parts.join('\r\n')
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return (pendingData, encoding, callback) => {
|
|
676
|
+
if (Array.isArray(pendingData)) {
|
|
677
|
+
pendingData[0].chunk = transformRequestMessage(
|
|
678
|
+
pendingData[0].chunk,
|
|
679
|
+
pendingData[0].encoding
|
|
680
|
+
)
|
|
681
|
+
} else {
|
|
682
|
+
pendingData = transformRequestMessage(pendingData, encoding)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
callback(pendingData)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|