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