@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.
- package/lib/browser/interceptors/WebSocket/index.js +14 -1
- package/lib/browser/interceptors/WebSocket/index.js.map +1 -1
- package/lib/browser/interceptors/fetch/web.js +1 -1
- package/lib/browser/presets/browser.js +1 -1
- package/lib/browser/{web-CdcYFjgm.js → web-DUlFZEtz.js} +58 -3
- package/lib/browser/web-DUlFZEtz.js.map +1 -0
- package/lib/node/{batch-interceptor-ByEZVih3.js → batch-interceptor-DMOfd-9x.js} +2 -2
- package/lib/node/{batch-interceptor-ByEZVih3.js.map → batch-interceptor-DMOfd-9x.js.map} +1 -1
- package/lib/node/buffer-utils-B4FUq-l2.js +17 -0
- package/lib/node/buffer-utils-B4FUq-l2.js.map +1 -0
- package/lib/node/create-request-id-DHo-fRTV.js +14 -0
- package/lib/node/create-request-id-DHo-fRTV.js.map +1 -0
- package/lib/node/{fetch-utils-Dw1PsmtX.js → fetch-utils-D-_xeRlK.js} +3 -14
- package/lib/node/{fetch-utils-Dw1PsmtX.js.map → fetch-utils-D-_xeRlK.js.map} +1 -1
- package/lib/node/{has-configurable-global-BU1sT1u4.js → has-configurable-global-CT6-QYdg.js} +2 -2
- package/lib/node/{has-configurable-global-BU1sT1u4.js.map → has-configurable-global-CT6-QYdg.js.map} +1 -1
- package/lib/node/index.d.ts +2 -243
- package/lib/node/index.js +6 -25
- package/lib/node/index.js.map +1 -1
- package/lib/node/{buffer-utils-BvPY1Tc-.js → interceptor-C8qRPjxG.js} +2 -16
- package/lib/node/interceptor-C8qRPjxG.js.map +1 -0
- package/lib/node/interceptors/ClientRequest/index.js +3 -3
- package/lib/node/interceptors/WebSocket/index.d.ts +2 -0
- package/lib/node/interceptors/WebSocket/index.js +635 -0
- package/lib/node/interceptors/WebSocket/index.js.map +1 -0
- package/lib/node/interceptors/XMLHttpRequest/node.js +5 -5
- package/lib/node/interceptors/fetch/node.js +5 -5
- package/lib/node/interceptors/http/index.js +2 -2
- package/lib/node/interceptors/net/index.d.ts +16 -0
- package/lib/node/interceptors/net/index.js +1 -1
- package/lib/node/{net-Ca8p1rIe.js → net-Bh16MP4u.js} +82 -92
- package/lib/node/net-Bh16MP4u.js.map +1 -0
- package/lib/node/patches-registry-DxR5TEc-.js +76 -0
- package/lib/node/patches-registry-DxR5TEc-.js.map +1 -0
- package/lib/node/remote-http-interceptor.js +4 -4
- package/lib/node/resolve-web-socket-url-CSvNPLGi.js +25 -0
- package/lib/node/resolve-web-socket-url-CSvNPLGi.js.map +1 -0
- package/lib/node/{source-CIgPng7r.js → source-DHVO1vzq.js} +327 -213
- package/lib/node/source-DHVO1vzq.js.map +1 -0
- package/lib/node/websocket-CC0nB0md.d.ts +285 -0
- package/package.json +5 -2
- package/src/interceptors/WebSocket/index.ts +4 -1
- package/src/interceptors/fetch/web.ts +11 -2
- package/src/interceptors/http/http-parser/index.ts +9 -1
- package/src/interceptors/http/http-parser.ts +21 -4
- package/src/interceptors/http/source.ts +368 -283
- package/src/interceptors/net/index.ts +39 -12
- package/src/interceptors/net/socket-controller.ts +126 -66
- package/src/utils/clone-response.ts +72 -0
- package/src/utils/internal-connection.ts +22 -0
- package/lib/browser/web-CdcYFjgm.js.map +0 -1
- package/lib/node/buffer-utils-BvPY1Tc-.js.map +0 -1
- package/lib/node/net-Ca8p1rIe.js.map +0 -1
- 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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
107
|
-
*
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
const onRequestData = (chunk: Buffer) => {
|
|
134
|
+
if (isHttpConnection === false) {
|
|
135
|
+
socketController.decline()
|
|
136
|
+
return
|
|
137
|
+
}
|
|
116
138
|
|
|
117
139
|
/**
|
|
118
|
-
* @note
|
|
119
|
-
* The
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
port: Number(tunnelUrl.port) || 80,
|
|
128
|
-
path: null,
|
|
129
|
-
})
|
|
130
|
-
}
|
|
145
|
+
if (tunnelUrl && !requestParser) {
|
|
146
|
+
isHttpConnection = undefined
|
|
131
147
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
163
|
+
if (requestParser) {
|
|
164
|
+
executeRequestParser(requestParser, toBuffer(chunk))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
139
167
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
200
|
-
|
|
181
|
+
const baseUrl =
|
|
182
|
+
tunnelUrl ?? connectionOptionsToUrl(connectionOptions, socket)
|
|
201
183
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
184
|
+
httpLogger.verbose('handling http message %o', {
|
|
185
|
+
httpMessage,
|
|
186
|
+
httpMethod,
|
|
187
|
+
baseUrl,
|
|
188
|
+
})
|
|
206
189
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
278
|
-
*
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
|
359
|
+
'HTTP response parser parsed: %d %s',
|
|
360
|
+
response.status,
|
|
361
|
+
response.statusText
|
|
318
362
|
)
|
|
319
363
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
364
|
+
if (isResponseError(response)) {
|
|
365
|
+
httpLogger.verbose(
|
|
366
|
+
'response is an error response, uncorking socket reads...'
|
|
367
|
+
)
|
|
323
368
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
{
|