@mswjs/interceptors 0.42.4 → 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/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/{fetch-utils-Tm5LbwBe.js → fetch-utils-D-_xeRlK.js} +2 -2
- package/lib/node/{fetch-utils-Tm5LbwBe.js.map → fetch-utils-D-_xeRlK.js.map} +1 -1
- package/lib/node/index.js +1 -1
- package/lib/node/interceptors/ClientRequest/index.js +1 -1
- package/lib/node/interceptors/XMLHttpRequest/node.js +2 -2
- package/lib/node/interceptors/fetch/node.js +2 -2
- package/lib/node/interceptors/http/index.js +1 -1
- package/lib/node/interceptors/net/index.d.ts +7 -0
- package/lib/node/interceptors/net/index.js +1 -1
- package/lib/node/{net-DkiHxhQF.js → net-Bh16MP4u.js} +40 -14
- package/lib/node/net-Bh16MP4u.js.map +1 -0
- package/lib/node/remote-http-interceptor.js +2 -2
- package/lib/node/{source-BFZ6wg4P.js → source-DHVO1vzq.js} +297 -240
- package/lib/node/source-DHVO1vzq.js.map +1 -0
- package/package.json +1 -1
- package/src/interceptors/fetch/web.ts +11 -2
- package/src/interceptors/http/source.ts +341 -324
- package/src/interceptors/net/socket-controller.ts +92 -57
- package/src/utils/clone-response.ts +72 -0
- package/lib/browser/web-CdcYFjgm.js.map +0 -1
- package/lib/node/net-DkiHxhQF.js.map +0 -1
- package/lib/node/source-BFZ6wg4P.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
|
|
|
@@ -111,378 +112,394 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
111
112
|
realSocketDestroy(error, callback)
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
}
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/**
|
|
125
|
-
* @note
|
|
126
|
-
*
|
|
127
|
-
* the tunnel target. The parser stopped at the tunnel boundary
|
|
128
|
-
* (HTTP upgrade semantics), so tear it down and detect the
|
|
129
|
-
* 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.
|
|
130
132
|
*/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
const onRequestData = (chunk: Buffer) => {
|
|
134
|
+
if (isHttpConnection === false) {
|
|
135
|
+
socketController.decline()
|
|
136
|
+
return
|
|
137
|
+
}
|
|
135
138
|
|
|
136
139
|
/**
|
|
137
|
-
* @note
|
|
138
|
-
* The
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* established this tunnel — like a real established tunnel
|
|
142
|
-
* 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.
|
|
143
144
|
*/
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
port: Number(tunnelUrl.port) || 80,
|
|
147
|
-
path: null,
|
|
148
|
-
})
|
|
149
|
-
}
|
|
145
|
+
if (tunnelUrl && !requestParser) {
|
|
146
|
+
isHttpConnection = undefined
|
|
150
147
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
}
|
|
155
162
|
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
if (requestParser) {
|
|
164
|
+
executeRequestParser(requestParser, toBuffer(chunk))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
158
167
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (!METHODS.includes(httpMethod.toUpperCase())) {
|
|
162
|
-
isHttpConnection = false
|
|
163
|
-
socketController.decline()
|
|
164
|
-
return
|
|
165
|
-
}
|
|
168
|
+
const httpMessage = chunk.toString()
|
|
169
|
+
const httpMethod = httpMessage.split(' ')[0] || ''
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// back to the context captured at the connection time, then
|
|
180
|
-
// to the underlying socket.
|
|
181
|
-
const requestContextValue =
|
|
182
|
-
requestContext.getStore() ?? connectionRequestContext
|
|
183
|
-
const initiator = requestContextValue?.initiator || socket
|
|
184
|
-
|
|
185
|
-
requestParser = new HttpRequestParser({
|
|
186
|
-
onError: stopParsingRequests,
|
|
187
|
-
connectionOptions: {
|
|
188
|
-
method: httpMethod,
|
|
189
|
-
url: baseUrl,
|
|
190
|
-
},
|
|
191
|
-
/**
|
|
192
|
-
* @note The message boundary ends the current exchange.
|
|
193
|
-
* Schedule the controller reset so the next write on this
|
|
194
|
-
* (kept-alive) socket opens a new exchange and buffers for
|
|
195
|
-
* its own verdict instead of following the settled one
|
|
196
|
-
* (e.g. leaking a mocked request to the server of a
|
|
197
|
-
* previously passed-through exchange).
|
|
198
|
-
*/
|
|
199
|
-
onMessageComplete: () => {
|
|
200
|
-
socketController.scheduleReset()
|
|
201
|
-
},
|
|
202
|
-
onRequest: async (parsedRequest, requestAbortController) => {
|
|
203
|
-
const request =
|
|
204
|
-
requestContextValue?.transformRequest?.(parsedRequest) ??
|
|
205
|
-
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
|
+
}
|
|
178
|
+
|
|
179
|
+
isHttpConnection = true
|
|
180
|
+
|
|
181
|
+
const baseUrl =
|
|
182
|
+
tunnelUrl ?? connectionOptionsToUrl(connectionOptions, socket)
|
|
206
183
|
|
|
184
|
+
httpLogger.verbose('handling http message %o', {
|
|
185
|
+
httpMessage,
|
|
186
|
+
httpMethod,
|
|
187
|
+
baseUrl,
|
|
188
|
+
})
|
|
189
|
+
|
|
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
|
+
},
|
|
207
203
|
/**
|
|
208
|
-
* @note
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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).
|
|
214
210
|
*/
|
|
215
|
-
|
|
216
|
-
socketController.
|
|
217
|
-
}
|
|
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
|
+
}
|
|
218
232
|
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
})
|
|
221
250
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
251
|
+
/**
|
|
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.
|
|
255
|
+
*/
|
|
256
|
+
if (socket.destroyed) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
226
259
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
httpLogger.verbose('respondWith() %o', {
|
|
232
|
-
status: rawResponse.status,
|
|
233
|
-
statusText: rawResponse.statusText,
|
|
234
|
-
hasBody: rawResponse.body != null,
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* @note The client may destroy the socket (e.g. on request
|
|
239
|
-
* abort) moments before a response arrives. A destroyed
|
|
240
|
-
* socket cannot be claimed and has no one reading it.
|
|
241
|
-
*/
|
|
242
|
-
if (socket.destroyed) {
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
socketController.claim()
|
|
247
|
-
|
|
248
|
-
const response = FetchResponse.from(rawResponse, {
|
|
249
|
-
url: request.url,
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* @note A successful mocked response to a "CONNECT"
|
|
254
|
-
* request establishes a tunnel to the requested authority
|
|
255
|
-
* (e.g. "127.0.0.1:80"). The exchange that follows on this
|
|
256
|
-
* socket is addressed to that authority, not to the proxy.
|
|
257
|
-
*/
|
|
258
|
-
if (request.method === 'CONNECT' && response.ok) {
|
|
259
|
-
tunnelUrl = new URL(`http://${request.url}`)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* @note Clone the response before "respondWith" because it will
|
|
264
|
-
* consume its body. This way, we can have a readable response copy
|
|
265
|
-
* for the "response" event below.
|
|
266
|
-
*/
|
|
267
|
-
const responseClone = isResponseError(response)
|
|
268
|
-
? null
|
|
269
|
-
: response.clone()
|
|
270
|
-
|
|
271
|
-
const respond = () => {
|
|
272
|
-
return this.respondWith({
|
|
273
|
-
socket: socketController[kRawSocket],
|
|
274
|
-
request: context.request,
|
|
275
|
-
response,
|
|
260
|
+
socketController.claim()
|
|
261
|
+
|
|
262
|
+
const originalResponse = FetchResponse.from(rawResponse, {
|
|
263
|
+
url: request.url,
|
|
276
264
|
})
|
|
277
|
-
|
|
265
|
+
const [response, responseClone] =
|
|
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
|
+
}
|
|
278
281
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
initiator,
|
|
283
|
-
requestId,
|
|
282
|
+
const respond = () => {
|
|
283
|
+
return this.respondWith({
|
|
284
|
+
socket: socketController[kRawSocket],
|
|
284
285
|
request: context.request,
|
|
285
|
-
response
|
|
286
|
-
responseType: 'mock',
|
|
286
|
+
response,
|
|
287
287
|
})
|
|
288
|
-
|
|
289
|
-
}
|
|
288
|
+
}
|
|
290
289
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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)
|
|
322
324
|
)
|
|
323
325
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
354
358
|
httpLogger.verbose(
|
|
355
|
-
'response
|
|
359
|
+
'HTTP response parser parsed: %d %s',
|
|
360
|
+
response.status,
|
|
361
|
+
response.statusText
|
|
356
362
|
)
|
|
357
363
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
364
|
+
if (isResponseError(response)) {
|
|
365
|
+
httpLogger.verbose(
|
|
366
|
+
'response is an error response, uncorking socket reads...'
|
|
367
|
+
)
|
|
361
368
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
httpLogger.verbose('emitting "response" event')
|
|
366
|
-
await this.emitter.emitAsPromise(
|
|
367
|
-
new HttpResponseEvent({
|
|
368
|
-
initiator,
|
|
369
|
-
requestId,
|
|
370
|
-
request: context.request,
|
|
371
|
-
response,
|
|
372
|
-
responseType: 'original',
|
|
373
|
-
})
|
|
374
|
-
)
|
|
375
|
-
} finally {
|
|
376
|
-
httpLogger.verbose('uncorking socket reads')
|
|
377
|
-
socketController.uncorkReads()
|
|
369
|
+
socketController.uncorkReads()
|
|
370
|
+
return
|
|
371
|
+
}
|
|
378
372
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
|
391
403
|
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
})
|
|
404
|
+
},
|
|
405
|
+
})
|
|
395
406
|
|
|
396
|
-
|
|
397
|
-
|
|
407
|
+
const onResponseData = (chunk: Buffer) => {
|
|
408
|
+
responseParser.execute(chunk)
|
|
398
409
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
410
|
+
// Free only after llhttp returns from its callbacks.
|
|
411
|
+
if (responseComplete) {
|
|
412
|
+
disposeResponseParser()
|
|
413
|
+
}
|
|
402
414
|
}
|
|
403
|
-
}
|
|
404
415
|
|
|
405
|
-
|
|
406
|
-
|
|
416
|
+
const onResponseEnd = () => {
|
|
417
|
+
disposeResponseParser()
|
|
418
|
+
|
|
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
|
+
}
|
|
407
425
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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)
|
|
412
432
|
}
|
|
413
|
-
}
|
|
414
433
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
responseParser.free(error)
|
|
434
|
+
realSocket
|
|
435
|
+
.on('data', onResponseData)
|
|
436
|
+
.once('end', onResponseEnd)
|
|
437
|
+
.once('close', onResponseEnd)
|
|
420
438
|
}
|
|
421
|
-
|
|
422
|
-
realSocket
|
|
423
|
-
.on('data', onResponseData)
|
|
424
|
-
.once('close', onResponseClose)
|
|
425
|
-
}
|
|
439
|
+
},
|
|
426
440
|
},
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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,
|
|
430
462
|
requestId,
|
|
463
|
+
request,
|
|
464
|
+
controller: requestController,
|
|
465
|
+
emitter: this.emitter,
|
|
466
|
+
logger: requestLogger,
|
|
431
467
|
}
|
|
432
|
-
)
|
|
433
468
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
+
}
|
|
441
483
|
|
|
442
|
-
|
|
443
|
-
* @note Create a request resolution context.
|
|
444
|
-
* This is so modifications to the "request" in upstream interceptors
|
|
445
|
-
* are correctly picked up by the underlying HTTP interceptor.
|
|
446
|
-
*/
|
|
447
|
-
const context: HandleRequestOptions = {
|
|
448
|
-
initiator,
|
|
449
|
-
requestId,
|
|
450
|
-
request,
|
|
451
|
-
controller: requestController,
|
|
452
|
-
emitter: this.emitter,
|
|
453
|
-
logger: requestLogger,
|
|
454
|
-
}
|
|
484
|
+
pendingRequestController = requestController
|
|
455
485
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
* controller become controlled errors.
|
|
462
|
-
*/
|
|
463
|
-
abortPendingRequest = () => {
|
|
464
|
-
if (
|
|
465
|
-
requestController.readyState === RequestController.PENDING
|
|
466
|
-
) {
|
|
467
|
-
requestAbortController.abort()
|
|
486
|
+
try {
|
|
487
|
+
await handleRequest(context)
|
|
488
|
+
} finally {
|
|
489
|
+
pendingRequestController = undefined
|
|
490
|
+
abortPendingRequest = undefined
|
|
468
491
|
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
pendingRequestController = requestController
|
|
492
|
+
},
|
|
493
|
+
})
|
|
472
494
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
pendingRequestController = undefined
|
|
477
|
-
abortPendingRequest = undefined
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
})
|
|
495
|
+
// Forward the first frame to the parser.
|
|
496
|
+
executeRequestParser(requestParser, toBuffer(chunk))
|
|
497
|
+
}
|
|
481
498
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
})
|
|
499
|
+
socket.on('data', onRequestData)
|
|
500
|
+
}
|
|
485
501
|
|
|
502
|
+
addRequestDataListener()
|
|
486
503
|
socket.on('close', () => requestParser?.free())
|
|
487
504
|
},
|
|
488
505
|
{
|