@mswjs/interceptors 0.42.4 → 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/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 -20
- package/lib/node/interceptors/net/index.js +1 -1
- package/lib/node/{net-DkiHxhQF.js → net-B3HDxRlp.js} +58 -66
- package/lib/node/net-B3HDxRlp.js.map +1 -0
- package/lib/node/remote-http-interceptor.js +2 -2
- package/lib/node/{source-BFZ6wg4P.js → source-C_hSJzNQ.js} +88 -29
- package/lib/node/source-C_hSJzNQ.js.map +1 -0
- package/package.json +1 -1
- package/src/interceptors/fetch/web.ts +11 -2
- package/src/interceptors/http/source.ts +56 -32
- package/src/interceptors/net/index.ts +18 -16
- package/src/interceptors/net/socket-controller.ts +95 -112
- 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
|
|
|
@@ -69,6 +70,20 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
69
70
|
let abortPendingRequest: (() => void) | undefined
|
|
70
71
|
let pendingRequestController: RequestController | undefined
|
|
71
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
|
+
|
|
72
87
|
// A malformed request loses the boundary for subsequent requests.
|
|
73
88
|
// Preserve the original bytes for the client and server to handle.
|
|
74
89
|
const stopParsingRequests = (error: Error) => {
|
|
@@ -80,7 +95,7 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
80
95
|
) {
|
|
81
96
|
void pendingRequestController.passthrough()
|
|
82
97
|
} else {
|
|
83
|
-
|
|
98
|
+
passthroughNonHttp()
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
requestParser?.free(error)
|
|
@@ -111,26 +126,36 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
111
126
|
realSocketDestroy(error, callback)
|
|
112
127
|
}
|
|
113
128
|
|
|
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
|
+
|
|
114
142
|
/**
|
|
115
|
-
* @note
|
|
116
|
-
*
|
|
143
|
+
* @note Inspect the first sent packet to determine the protocol,
|
|
144
|
+
* including when entering a mocked "CONNECT" tunnel.
|
|
117
145
|
*/
|
|
118
|
-
|
|
146
|
+
const onRequestData = (chunk: Buffer) => {
|
|
119
147
|
if (isHttpConnection === false) {
|
|
120
|
-
|
|
148
|
+
passthroughNonHttp()
|
|
121
149
|
return
|
|
122
150
|
}
|
|
123
151
|
|
|
124
152
|
/**
|
|
125
153
|
* @note A mocked "CONNECT" request has established a tunnel.
|
|
126
154
|
* The data that follows belongs to a new exchange addressed to
|
|
127
|
-
* the tunnel target. The parser
|
|
128
|
-
*
|
|
129
|
-
* tunneled protocol anew, like on a fresh connection.
|
|
155
|
+
* the tunnel target. The previous parser was freed at the upgrade
|
|
156
|
+
* boundary, so detect the tunneled protocol anew.
|
|
130
157
|
*/
|
|
131
|
-
if (tunnelUrl && requestParser) {
|
|
132
|
-
requestParser.free()
|
|
133
|
-
requestParser = undefined
|
|
158
|
+
if (tunnelUrl && !requestParser) {
|
|
134
159
|
isHttpConnection = undefined
|
|
135
160
|
|
|
136
161
|
/**
|
|
@@ -149,18 +174,17 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
if (requestParser) {
|
|
152
|
-
requestParser
|
|
177
|
+
executeRequestParser(requestParser, toBuffer(chunk))
|
|
153
178
|
return
|
|
154
179
|
}
|
|
155
180
|
|
|
156
181
|
const httpMessage = chunk.toString()
|
|
157
182
|
const httpMethod = httpMessage.split(' ')[0] || ''
|
|
158
183
|
|
|
159
|
-
//
|
|
160
|
-
// pass them through once every subscriber has declined.
|
|
184
|
+
// Stop parsing connections that do not carry HTTP traffic.
|
|
161
185
|
if (!METHODS.includes(httpMethod.toUpperCase())) {
|
|
162
186
|
isHttpConnection = false
|
|
163
|
-
|
|
187
|
+
passthroughNonHttp()
|
|
164
188
|
return
|
|
165
189
|
}
|
|
166
190
|
|
|
@@ -245,9 +269,14 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
245
269
|
|
|
246
270
|
socketController.claim()
|
|
247
271
|
|
|
248
|
-
const
|
|
272
|
+
const originalResponse = FetchResponse.from(rawResponse, {
|
|
249
273
|
url: request.url,
|
|
250
274
|
})
|
|
275
|
+
const [response, responseClone] =
|
|
276
|
+
!isResponseError(originalResponse) &&
|
|
277
|
+
this.emitter.listenerCount('response') > 0
|
|
278
|
+
? cloneResponse(originalResponse)
|
|
279
|
+
: [originalResponse, null]
|
|
251
280
|
|
|
252
281
|
/**
|
|
253
282
|
* @note A successful mocked response to a "CONNECT"
|
|
@@ -257,17 +286,9 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
257
286
|
*/
|
|
258
287
|
if (request.method === 'CONNECT' && response.ok) {
|
|
259
288
|
tunnelUrl = new URL(`http://${request.url}`)
|
|
289
|
+
socket.on('data', onRequestData)
|
|
260
290
|
}
|
|
261
291
|
|
|
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
292
|
const respond = () => {
|
|
272
293
|
return this.respondWith({
|
|
273
294
|
socket: socketController[kRawSocket],
|
|
@@ -402,11 +423,11 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
402
423
|
}
|
|
403
424
|
}
|
|
404
425
|
|
|
405
|
-
const
|
|
426
|
+
const onResponseEnd = () => {
|
|
406
427
|
disposeResponseParser()
|
|
407
428
|
|
|
408
|
-
// Without a response,
|
|
409
|
-
//
|
|
429
|
+
// Without a final response, release EOF now. A half-open
|
|
430
|
+
// socket cannot close until the client consumes it.
|
|
410
431
|
if (!hasFinalResponse) {
|
|
411
432
|
socketController.uncorkReads()
|
|
412
433
|
}
|
|
@@ -415,13 +436,15 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
415
436
|
const disposeResponseParser = (error?: Error) => {
|
|
416
437
|
responseParserDisposed = true
|
|
417
438
|
realSocket.removeListener('data', onResponseData)
|
|
418
|
-
realSocket.removeListener('
|
|
439
|
+
realSocket.removeListener('end', onResponseEnd)
|
|
440
|
+
realSocket.removeListener('close', onResponseEnd)
|
|
419
441
|
responseParser.free(error)
|
|
420
442
|
}
|
|
421
443
|
|
|
422
444
|
realSocket
|
|
423
445
|
.on('data', onResponseData)
|
|
424
|
-
.once('
|
|
446
|
+
.once('end', onResponseEnd)
|
|
447
|
+
.once('close', onResponseEnd)
|
|
425
448
|
}
|
|
426
449
|
},
|
|
427
450
|
},
|
|
@@ -480,9 +503,10 @@ export class NodeHttpRequestSource extends Interceptor<HttpRequestEventMap> {
|
|
|
480
503
|
})
|
|
481
504
|
|
|
482
505
|
// Forward the first frame to the parser.
|
|
483
|
-
requestParser
|
|
484
|
-
}
|
|
506
|
+
executeRequestParser(requestParser, toBuffer(chunk))
|
|
507
|
+
}
|
|
485
508
|
|
|
509
|
+
socket.on('data', onRequestData)
|
|
486
510
|
socket.on('close', () => requestParser?.free())
|
|
487
511
|
},
|
|
488
512
|
{
|
|
@@ -271,26 +271,28 @@ export class SocketInterceptor extends Interceptor<SocketEventMap> {
|
|
|
271
271
|
})
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
process.nextTick(() => {
|
|
274
|
+
process.nextTick(async () => {
|
|
275
275
|
if (socket.destroyed) {
|
|
276
276
|
return
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
279
|
+
if (interceptor.listenerCount('connection') === 0) {
|
|
280
|
+
controller.passthrough()
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await interceptor.emitter.emitAsPromise(
|
|
286
|
+
new SocketConnectionEvent({
|
|
287
|
+
socket: controller.serverSocket,
|
|
288
|
+
controller,
|
|
289
|
+
connectionOptions,
|
|
290
|
+
})
|
|
291
|
+
)
|
|
292
|
+
} catch (error) {
|
|
293
|
+
socket.destroy(error as Error)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
294
296
|
|
|
295
297
|
logger.verbose('emitted "connection" event!')
|
|
296
298
|
})
|
|
@@ -239,9 +239,10 @@ function toServerSocket<T extends net.Socket>(socket: T): T {
|
|
|
239
239
|
listener(toBuffer(chunk, encoding))
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// Writable so a removed listener can be added again.
|
|
242
243
|
Object.defineProperty(listener, kListenerWrap, {
|
|
243
244
|
enumerable: false,
|
|
244
|
-
writable:
|
|
245
|
+
writable: true,
|
|
245
246
|
value: listenerWrap,
|
|
246
247
|
})
|
|
247
248
|
|
|
@@ -365,8 +366,6 @@ export abstract class SocketController {
|
|
|
365
366
|
| typeof SocketController.CLAIMED
|
|
366
367
|
| typeof SocketController.PASSTHROUGH
|
|
367
368
|
|
|
368
|
-
#awaitedVerdicts = 0
|
|
369
|
-
|
|
370
369
|
private [kRawSocket]: net.Socket
|
|
371
370
|
|
|
372
371
|
constructor(socket: net.Socket) {
|
|
@@ -405,55 +404,6 @@ export abstract class SocketController {
|
|
|
405
404
|
|
|
406
405
|
this.readyState = SocketController.PASSTHROUGH
|
|
407
406
|
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Await a verdict on this connection from the given number of
|
|
411
|
-
* subscribers. A connection nobody awaits to inspect (or one that
|
|
412
|
-
* every awaited subscriber has declined) is passed through as-is.
|
|
413
|
-
* This makes "unclaimed after everyone declined" a state owned by
|
|
414
|
-
* the controller instead of the individual subscribers.
|
|
415
|
-
*/
|
|
416
|
-
public awaitVerdicts(count: number): void {
|
|
417
|
-
this.#awaitedVerdicts = count
|
|
418
|
-
|
|
419
|
-
if (this.#awaitedVerdicts === 0) {
|
|
420
|
-
this.passthrough()
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Decline this socket connection. Declining means the subscriber
|
|
426
|
-
* has inspected the connection and will not handle it (e.g. the
|
|
427
|
-
* traffic is not of the protocol that subscriber implements).
|
|
428
|
-
* Once every awaited subscriber declines, the connection is
|
|
429
|
-
* passed through as-is.
|
|
430
|
-
*/
|
|
431
|
-
public decline(): void {
|
|
432
|
-
if (this.readyState !== SocketController.PENDING) {
|
|
433
|
-
return
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
this.#awaitedVerdicts -= 1
|
|
437
|
-
|
|
438
|
-
if (this.#awaitedVerdicts <= 0) {
|
|
439
|
-
/**
|
|
440
|
-
* @note Defer the passthrough so it never transitions this
|
|
441
|
-
* controller in the middle of a client write. Declines are
|
|
442
|
-
* issued while the written data is being pushed to the server
|
|
443
|
-
* socket, and a synchronous transition would race the write's
|
|
444
|
-
* own bookkeeping (e.g. re-buffering the write after a reset
|
|
445
|
-
* at an exchange boundary).
|
|
446
|
-
*/
|
|
447
|
-
process.nextTick(() => {
|
|
448
|
-
if (
|
|
449
|
-
this.readyState === SocketController.PENDING &&
|
|
450
|
-
!this[kRawSocket].destroyed
|
|
451
|
-
) {
|
|
452
|
-
this.passthrough()
|
|
453
|
-
}
|
|
454
|
-
})
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
407
|
}
|
|
458
408
|
|
|
459
409
|
export type FlushPendingDataFunction = (
|
|
@@ -472,6 +422,8 @@ export class TcpSocketController extends SocketController {
|
|
|
472
422
|
|
|
473
423
|
protected pendingConnection: PromiseWithResolvers<[TcpWrap, TcpHandle]>
|
|
474
424
|
|
|
425
|
+
#removePassthroughSocketListeners?: () => void
|
|
426
|
+
|
|
475
427
|
#connectionOptions?: NetworkConnectionOptions
|
|
476
428
|
#retargetedConnectionOptions?: NetworkConnectionOptions &
|
|
477
429
|
net.SocketConnectOpts
|
|
@@ -599,9 +551,7 @@ export class TcpSocketController extends SocketController {
|
|
|
599
551
|
* authority of an established "CONNECT" tunnel). An unclaimed
|
|
600
552
|
* exchange then passes through to that target instead of the
|
|
601
553
|
* originally dialed one, and a claimed exchange reports it as the
|
|
602
|
-
* peer.
|
|
603
|
-
* that declined this connection's protocol stay declined across
|
|
604
|
-
* the exchanges, retargeted or not.
|
|
554
|
+
* peer.
|
|
605
555
|
*/
|
|
606
556
|
public reset(
|
|
607
557
|
connectionOptions?: NetworkConnectionOptions & net.SocketConnectOpts
|
|
@@ -618,21 +568,8 @@ export class TcpSocketController extends SocketController {
|
|
|
618
568
|
* must not close the client socket).
|
|
619
569
|
*/
|
|
620
570
|
if (this.#passthroughSocket) {
|
|
621
|
-
this.#
|
|
622
|
-
|
|
623
|
-
.removeListener(
|
|
624
|
-
'connectionAttemptFailed',
|
|
625
|
-
this.#onRealSocketConnectionAttemptFailed
|
|
626
|
-
)
|
|
627
|
-
.removeListener(
|
|
628
|
-
'connectionAttemptTimeout',
|
|
629
|
-
this.#onRealSocketConnectionAttemptTimeout
|
|
630
|
-
)
|
|
631
|
-
.removeListener('data', this.#onRealSocketData)
|
|
632
|
-
.removeListener('error', this.#onRealSocketError)
|
|
633
|
-
.removeListener('end', this.#onRealSocketEnd)
|
|
634
|
-
.removeListener('close', this.#onRealSocketClose)
|
|
635
|
-
.destroy()
|
|
571
|
+
this.#removePassthroughSocketListeners?.()
|
|
572
|
+
this.#passthroughSocket.destroy()
|
|
636
573
|
|
|
637
574
|
this.#passthroughSocket = null
|
|
638
575
|
|
|
@@ -1325,6 +1262,11 @@ export class TcpSocketController extends SocketController {
|
|
|
1325
1262
|
// but this skips the detection cost on its every "destroyed" read).
|
|
1326
1263
|
realSocket[kPatched] = true
|
|
1327
1264
|
|
|
1265
|
+
// The client owns half-close semantics. It may not have consumed
|
|
1266
|
+
// the forwarded EOF yet, so keep the real connection writable until
|
|
1267
|
+
// the client ends its writable side.
|
|
1268
|
+
realSocket.allowHalfOpen = true
|
|
1269
|
+
|
|
1328
1270
|
if (this.socket.timeout != null) {
|
|
1329
1271
|
realSocket.setTimeout(this.socket.timeout)
|
|
1330
1272
|
}
|
|
@@ -1338,9 +1280,8 @@ export class TcpSocketController extends SocketController {
|
|
|
1338
1280
|
? this.#passthroughSocket
|
|
1339
1281
|
: createRealSocket()
|
|
1340
1282
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
1283
|
+
const isNewConnection = realSocket !== this.#passthroughSocket
|
|
1284
|
+
this.#passthroughSocket = realSocket
|
|
1344
1285
|
|
|
1345
1286
|
if (this.#bufferedWrites.length === 0) {
|
|
1346
1287
|
logger.verbose(
|
|
@@ -1378,32 +1319,15 @@ export class TcpSocketController extends SocketController {
|
|
|
1378
1319
|
this.socket.removeListener('drain', this.#onMockSocketDrain)
|
|
1379
1320
|
this.socket.on('drain', this.#onMockSocketDrain)
|
|
1380
1321
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
this
|
|
1386
|
-
)
|
|
1387
|
-
.removeListener(
|
|
1388
|
-
'connectionAttemptTimeout',
|
|
1389
|
-
this.#onRealSocketConnectionAttemptTimeout
|
|
1390
|
-
)
|
|
1391
|
-
.removeListener('data', this.#onRealSocketData)
|
|
1392
|
-
.removeListener('error', this.#onRealSocketError)
|
|
1393
|
-
.removeListener('end', this.#onRealSocketEnd)
|
|
1394
|
-
.removeListener('close', this.#onRealSocketClose)
|
|
1322
|
+
// Let Node register its pending-write "connect" listener first so
|
|
1323
|
+
// buffered writes flush before our listener swaps the socket handle.
|
|
1324
|
+
if (isNewConnection) {
|
|
1325
|
+
this.#removePassthroughSocketListeners =
|
|
1326
|
+
this.addPassthroughSocketListeners(realSocket)
|
|
1395
1327
|
|
|
1396
|
-
|
|
1397
|
-
.once('
|
|
1398
|
-
|
|
1399
|
-
.on(
|
|
1400
|
-
'connectionAttemptTimeout',
|
|
1401
|
-
this.#onRealSocketConnectionAttemptTimeout
|
|
1402
|
-
)
|
|
1403
|
-
.on('data', this.#onRealSocketData)
|
|
1404
|
-
.on('error', this.#onRealSocketError)
|
|
1405
|
-
.on('end', this.#onRealSocketEnd)
|
|
1406
|
-
.on('close', this.#onRealSocketClose)
|
|
1328
|
+
// The real socket may still emit errors after the client closes.
|
|
1329
|
+
realSocket.once('close', this.#removePassthroughSocketListeners)
|
|
1330
|
+
}
|
|
1407
1331
|
|
|
1408
1332
|
/**
|
|
1409
1333
|
* @note Forward the client's half-close, unless the real handle
|
|
@@ -1422,6 +1346,41 @@ export class TcpSocketController extends SocketController {
|
|
|
1422
1346
|
return realSocket
|
|
1423
1347
|
}
|
|
1424
1348
|
|
|
1349
|
+
/**
|
|
1350
|
+
* Forward events for the lifetime of the connection, including while
|
|
1351
|
+
* it is idle in an agent pool. Reusing it must not add more listeners.
|
|
1352
|
+
*/
|
|
1353
|
+
protected addPassthroughSocketListeners(realSocket: net.Socket): () => void {
|
|
1354
|
+
realSocket
|
|
1355
|
+
.once('connect', this.#onRealSocketConnect)
|
|
1356
|
+
.on('connectionAttemptFailed', this.#onRealSocketConnectionAttemptFailed)
|
|
1357
|
+
.on(
|
|
1358
|
+
'connectionAttemptTimeout',
|
|
1359
|
+
this.#onRealSocketConnectionAttemptTimeout
|
|
1360
|
+
)
|
|
1361
|
+
.on('data', this.#onRealSocketData)
|
|
1362
|
+
.on('error', this.#onRealSocketError)
|
|
1363
|
+
.on('end', this.#onRealSocketEnd)
|
|
1364
|
+
.on('close', this.#onRealSocketClose)
|
|
1365
|
+
|
|
1366
|
+
return () => {
|
|
1367
|
+
realSocket
|
|
1368
|
+
.removeListener('connect', this.#onRealSocketConnect)
|
|
1369
|
+
.removeListener(
|
|
1370
|
+
'connectionAttemptFailed',
|
|
1371
|
+
this.#onRealSocketConnectionAttemptFailed
|
|
1372
|
+
)
|
|
1373
|
+
.removeListener(
|
|
1374
|
+
'connectionAttemptTimeout',
|
|
1375
|
+
this.#onRealSocketConnectionAttemptTimeout
|
|
1376
|
+
)
|
|
1377
|
+
.removeListener('data', this.#onRealSocketData)
|
|
1378
|
+
.removeListener('error', this.#onRealSocketError)
|
|
1379
|
+
.removeListener('end', this.#onRealSocketEnd)
|
|
1380
|
+
.removeListener('close', this.#onRealSocketClose)
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1425
1384
|
/**
|
|
1426
1385
|
* Create the passthrough connection to the target this controller
|
|
1427
1386
|
* was retargeted to (see `reset()`). The original `createConnection`
|
|
@@ -1663,20 +1622,44 @@ export class TlsSocketController extends TcpSocketController {
|
|
|
1663
1622
|
}
|
|
1664
1623
|
}
|
|
1665
1624
|
|
|
1666
|
-
realSocket
|
|
1667
|
-
.on('secure', () => {
|
|
1668
|
-
this.socket.emit('secure')
|
|
1669
|
-
})
|
|
1670
|
-
.on('session', (...args) => {
|
|
1671
|
-
this.socket.emit('session', ...args)
|
|
1672
|
-
})
|
|
1673
|
-
.on('keylog', (...args) => {
|
|
1674
|
-
this.socket.emit('keylog', ...args)
|
|
1675
|
-
})
|
|
1676
|
-
.on('OCSPResponse', (...args) => {
|
|
1677
|
-
this.socket.emit('OCSPResponse', ...args)
|
|
1678
|
-
})
|
|
1679
|
-
|
|
1680
1625
|
return realSocket
|
|
1681
1626
|
}
|
|
1627
|
+
|
|
1628
|
+
#onRealSocketSecure = () => {
|
|
1629
|
+
this.socket.emit('secure')
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
#onRealSocketSession = (session: Buffer) => {
|
|
1633
|
+
this.socket.emit('session', session)
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
#onRealSocketKeylog = (line: Buffer) => {
|
|
1637
|
+
this.socket.emit('keylog', line)
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
#onRealSocketOCSPResponse = (response: Buffer | null) => {
|
|
1641
|
+
this.socket.emit('OCSPResponse', response)
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
protected addPassthroughSocketListeners(realSocket: net.Socket): () => void {
|
|
1645
|
+
const removeTcpSocketListeners = super.addPassthroughSocketListeners(
|
|
1646
|
+
realSocket
|
|
1647
|
+
)
|
|
1648
|
+
|
|
1649
|
+
realSocket
|
|
1650
|
+
.on('secure', this.#onRealSocketSecure)
|
|
1651
|
+
.on('session', this.#onRealSocketSession)
|
|
1652
|
+
.on('keylog', this.#onRealSocketKeylog)
|
|
1653
|
+
.on('OCSPResponse', this.#onRealSocketOCSPResponse)
|
|
1654
|
+
|
|
1655
|
+
return () => {
|
|
1656
|
+
removeTcpSocketListeners()
|
|
1657
|
+
|
|
1658
|
+
realSocket
|
|
1659
|
+
.removeListener('secure', this.#onRealSocketSecure)
|
|
1660
|
+
.removeListener('session', this.#onRealSocketSession)
|
|
1661
|
+
.removeListener('keylog', this.#onRealSocketKeylog)
|
|
1662
|
+
.removeListener('OCSPResponse', this.#onRealSocketOCSPResponse)
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1682
1665
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { FetchResponse } from './fetch-utils'
|
|
2
|
+
import { copyRawHeaders } from '../interceptors/ClientRequest/utils/record-raw-headers'
|
|
3
|
+
|
|
4
|
+
/** Clone for observers without letting their unread body block caller cancellation. */
|
|
5
|
+
export function cloneResponse(response: Response): [Response, Response] {
|
|
6
|
+
const clone = FetchResponse.clone(response)
|
|
7
|
+
|
|
8
|
+
if (!response.body || !clone.body) {
|
|
9
|
+
return [response, clone]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const observer = wrapResponse(clone)
|
|
13
|
+
const caller = wrapResponse(response, observer.cancel)
|
|
14
|
+
|
|
15
|
+
return [caller.response, observer.response]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function wrapResponse(
|
|
19
|
+
response: Response,
|
|
20
|
+
onCancel?: (reason: unknown) => Promise<void>
|
|
21
|
+
) {
|
|
22
|
+
const body = response.body!
|
|
23
|
+
const reader = body.getReader()
|
|
24
|
+
const cancel = (reason: unknown) => {
|
|
25
|
+
return body.locked ? reader.cancel(reason) : body.cancel(reason)
|
|
26
|
+
}
|
|
27
|
+
const stream = new ReadableStream<Uint8Array>(
|
|
28
|
+
{
|
|
29
|
+
async pull(controller) {
|
|
30
|
+
try {
|
|
31
|
+
const { done, value } = await reader.read()
|
|
32
|
+
|
|
33
|
+
if (done) {
|
|
34
|
+
controller.close()
|
|
35
|
+
reader.releaseLock()
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
controller.enqueue(value)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
controller.error(error)
|
|
42
|
+
reader.releaseLock()
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async cancel(reason) {
|
|
46
|
+
try {
|
|
47
|
+
const cancellation = cancel(reason)
|
|
48
|
+
|
|
49
|
+
if (onCancel) {
|
|
50
|
+
// Caller cancellation owns both branches.
|
|
51
|
+
await Promise.all([cancellation, onCancel(reason)])
|
|
52
|
+
} else {
|
|
53
|
+
// An observer must not wait for the caller to consume its branch:
|
|
54
|
+
// Response delivery is still waiting for this listener to finish.
|
|
55
|
+
void cancellation.catch(() => {})
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
reader.releaseLock()
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{ highWaterMark: 0 }
|
|
63
|
+
)
|
|
64
|
+
const wrappedResponse = new FetchResponse(stream, response)
|
|
65
|
+
copyRawHeaders(response.headers, wrappedResponse.headers)
|
|
66
|
+
Object.defineProperties(wrappedResponse, {
|
|
67
|
+
type: { value: response.type },
|
|
68
|
+
redirected: { value: response.redirected },
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return { response: wrappedResponse, cancel }
|
|
72
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"web-CdcYFjgm.js","names":[],"sources":["../../src/interceptors/fetch/utils/create-network-error.ts","../../src/interceptors/fetch/utils/follow-redirect.ts","../../src/interceptors/fetch/utils/brotli-decompress.browser.ts","../../src/interceptors/fetch/utils/decompression.ts","../../src/interceptors/fetch/web.ts"],"sourcesContent":["export function createNetworkError(cause?: unknown) {\n return Object.assign(new TypeError('Failed to fetch'), {\n cause,\n })\n}\n","import { createNetworkError } from './create-network-error'\n\nconst REQUEST_BODY_HEADERS = [\n 'content-encoding',\n 'content-language',\n 'content-location',\n 'content-type',\n 'content-length',\n]\n\nconst kRedirectCount = Symbol('kRedirectCount')\n\n/**\n * @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1210\n */\nexport async function followFetchRedirect(\n request: Request,\n response: Response\n): Promise<Response> {\n if (response.status !== 303 && request.body != null) {\n return Promise.reject(createNetworkError())\n }\n\n const requestUrl = new URL(request.url)\n\n let locationUrl: URL\n try {\n // If the location is a relative URL, use the request URL as the base URL.\n locationUrl = new URL(response.headers.get('location')!, request.url) \n } catch (error) {\n return Promise.reject(createNetworkError(error))\n }\n\n if (\n !(locationUrl.protocol === 'http:' || locationUrl.protocol === 'https:')\n ) {\n return Promise.reject(\n createNetworkError('URL scheme must be a HTTP(S) scheme')\n )\n }\n\n if (Reflect.get(request, kRedirectCount) > 20) {\n return Promise.reject(createNetworkError('redirect count exceeded'))\n }\n\n Object.defineProperty(request, kRedirectCount, {\n value: (Reflect.get(request, kRedirectCount) || 0) + 1,\n })\n\n if (\n request.mode === 'cors' &&\n (locationUrl.username || locationUrl.password) &&\n !sameOrigin(requestUrl, locationUrl)\n ) {\n return Promise.reject(\n createNetworkError('cross origin not allowed for request mode \"cors\"')\n )\n }\n\n const requestInit: RequestInit = {}\n\n if (\n ([301, 302].includes(response.status) && request.method === 'POST') ||\n (response.status === 303 && !['HEAD', 'GET'].includes(request.method))\n ) {\n requestInit.method = 'GET'\n requestInit.body = null\n\n REQUEST_BODY_HEADERS.forEach((headerName) => {\n request.headers.delete(headerName)\n })\n }\n\n if (!sameOrigin(requestUrl, locationUrl)) {\n request.headers.delete('authorization')\n request.headers.delete('proxy-authorization')\n request.headers.delete('cookie')\n request.headers.delete('host')\n }\n\n /**\n * @note Undici \"safely\" extracts the request body.\n * I suspect we cannot dispatch this request again\n * since its body has been read and the stream is locked.\n */\n\n requestInit.headers = request.headers\n const finalResponse = await fetch(new Request(locationUrl, requestInit))\n Object.defineProperty(finalResponse, 'redirected', {\n value: true,\n configurable: true,\n })\n\n return finalResponse\n}\n\n/**\n * @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/util.js#L761\n */\nfunction sameOrigin(left: URL, right: URL): boolean {\n if (left.origin === right.origin && left.origin === 'null') {\n return true\n }\n\n if (\n left.protocol === right.protocol &&\n left.hostname === right.hostname &&\n left.port === right.port\n ) {\n return true\n }\n\n return false\n}\n","export class BrotliDecompressionStream extends TransformStream {\n constructor() {\n console.warn(\n '[Interceptors]: Brotli decompression of response streams is not supported in the browser'\n )\n\n super({\n transform(chunk, controller) {\n // Keep the stream as passthrough, it does nothing.\n controller.enqueue(chunk)\n },\n })\n }\n}\n","// Import from an internal alias that resolves to different modules\n// depending on the environment. This way, we can keep the fetch interceptor\n// intact while using different strategies for Brotli decompression.\nimport { BrotliDecompressionStream } from 'internal:brotli-decompress'\n\nclass PipelineStream extends TransformStream {\n constructor(\n transformStreams: Array<TransformStream>,\n ...strategies: Array<QueuingStrategy>\n ) {\n super({}, ...strategies)\n\n const readable = [super.readable as any, ...transformStreams].reduce(\n (readable, transform) => readable.pipeThrough(transform)\n )\n\n Object.defineProperty(this, 'readable', {\n get() {\n return readable\n },\n })\n }\n}\n\nexport function parseContentEncoding(contentEncoding: string): Array<string> {\n return contentEncoding\n .toLowerCase()\n .split(',')\n .map((coding) => coding.trim())\n}\n\nfunction createDecompressionStream(\n contentEncoding: string\n): TransformStream | null {\n if (contentEncoding === '') {\n return null\n }\n\n const codings = parseContentEncoding(contentEncoding)\n\n if (codings.length === 0) {\n return null\n }\n\n const transformers = codings.reduceRight<Array<TransformStream>>(\n (transformers, coding) => {\n if (coding === 'gzip' || coding === 'x-gzip') {\n return transformers.concat(new DecompressionStream('gzip'))\n } else if (coding === 'deflate') {\n return transformers.concat(new DecompressionStream('deflate'))\n } else if (coding === 'br') {\n return transformers.concat(new BrotliDecompressionStream())\n } else {\n transformers.length = 0\n }\n\n return transformers\n },\n []\n )\n\n return new PipelineStream(transformers)\n}\n\nexport function decompressResponse(\n response: Response\n): ReadableStream<any> | null {\n if (response.body === null) {\n return null\n }\n\n const decompressionStream = createDecompressionStream(\n response.headers.get('content-encoding') || ''\n )\n\n if (!decompressionStream) {\n return null\n }\n\n // Use `pipeTo` and return the decompression stream's readable\n // instead of `pipeThrough` because that will lock the original\n // response stream, making it unusable as the input to Response.\n response.body.pipeTo(decompressionStream.writable)\n return decompressionStream.readable\n}\n","import { until } from '@open-draft/until'\nimport { HttpResponseEvent, type HttpRequestEventMap } from '../../events/http'\nimport { RequestController } from '../../request-controller'\nimport { handleRequest } from '../../utils/handle-request'\nimport { createRequestId } from '../../create-request-id'\nimport { createNetworkError } from './utils/create-network-error'\nimport { followFetchRedirect } from './utils/follow-redirect'\nimport { decompressResponse } from './utils/decompression'\nimport { hasConfigurableGlobal } from '../../utils/has-configurable-global'\nimport { FetchResponse } from '../../utils/fetch-utils'\nimport { isResponseError } from '../../utils/response-utils'\nimport { patchesRegistry } from '../../utils/patches-registry'\nimport { copyRawHeaders } from '../ClientRequest/utils/record-raw-headers'\nimport { Interceptor } from '../../interceptor'\nimport { createLogger } from '../../utils/logger'\n\nconst logger = createLogger('fetch')\n\n/**\n * Interceptor for `fetch` requests in the browser.\n */\nexport class FetchInterceptor extends Interceptor<HttpRequestEventMap> {\n static symbol = Symbol.for('fetch-interceptor')\n\n protected predicate() {\n return hasConfigurableGlobal('fetch')\n }\n\n protected async setup() {\n logger.verbose('patching global fetch...')\n\n this.subscriptions.push(\n patchesRegistry.applyPatch(globalThis, 'fetch', (realFetch) => {\n return async (input, init) => {\n const requestId = createRequestId()\n\n /**\n * @note Resolve potentially relative request URL\n * against the present `location`. This is mainly\n * for native `fetch` in JSDOM.\n * @see https://github.com/mswjs/msw/issues/1625\n */\n const resolvedInput =\n typeof input === 'string' &&\n typeof location !== 'undefined' &&\n !URL.canParse(input)\n ? new URL(input, location.href)\n : input\n\n const request = new Request(resolvedInput, init)\n\n const responsePromise = Promise.withResolvers<Response>()\n\n const controller = new RequestController(\n request,\n {\n passthrough: async () => {\n logger.verbose('performing request as-is')\n\n /**\n * @note Clone the request instance right before performing it.\n * This preserves any modifications made to the intercepted request\n * in the \"request\" listener. This also allows the user to read the\n * request body in the \"response\" listener (otherwise \"unusable\").\n */\n const requestCloneForResponseEvent = request.clone()\n\n // Perform the intercepted request as-is.\n const [responseError, originalResponse] = await until(() =>\n realFetch(request)\n )\n\n if (responseError) {\n return responsePromise.reject(responseError)\n }\n\n logger.verbose('original fetch performed %o', originalResponse)\n\n if (this.emitter.listenerCount('response') > 0) {\n logger.verbose('emitting the \"response\" event')\n\n const responseClone = FetchResponse.clone(originalResponse)\n await this.emitter.emitAsPromise(\n new HttpResponseEvent({\n initiator: requestCloneForResponseEvent,\n request: requestCloneForResponseEvent,\n requestId,\n response: responseClone,\n responseType: 'original',\n })\n )\n }\n\n // Resolve the response promise with the original response\n // since the `fetch()` return this internal promise.\n responsePromise.resolve(originalResponse)\n },\n respondWith: async (rawResponse) => {\n // Handle mocked `Response.error()` (i.e. request errors).\n if (isResponseError(rawResponse)) {\n logger.verbose('request errored %o', {\n response: rawResponse,\n })\n responsePromise.reject(createNetworkError(rawResponse))\n return\n }\n\n // Decompress the mocked response body, if applicable.\n const decompressedStream = decompressResponse(rawResponse)\n const response = new FetchResponse(\n decompressedStream || rawResponse.body,\n {\n url: request.url,\n status: rawResponse.status,\n statusText: rawResponse.statusText,\n headers: rawResponse.headers,\n }\n )\n\n copyRawHeaders(rawResponse.headers, response.headers)\n\n /**\n * Undici's handling of following redirect responses.\n * Treat the \"manual\" redirect mode as a regular mocked response.\n * This way, the client can manually follow the redirect it receives.\n * @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1173\n */\n if (FetchResponse.isRedirectResponse(response.status)) {\n // Reject the request promise if its `redirect` is set to `error`\n // and it receives a mocked redirect response.\n if (request.redirect === 'error') {\n responsePromise.reject(\n createNetworkError('unexpected redirect')\n )\n return\n }\n\n if (request.redirect === 'follow') {\n followFetchRedirect(request, response).then(\n (response) => {\n responsePromise.resolve(response)\n },\n (reason) => {\n responsePromise.reject(reason)\n }\n )\n return\n }\n }\n\n if (this.emitter.listenerCount('response') > 0) {\n logger.verbose('emitting the \"response\" event')\n\n // Await the response listeners to finish before resolving\n // the response promise. This ensures all your logic finishes\n // before the interceptor resolves the pending response.\n await this.emitter.emitAsPromise(\n new HttpResponseEvent({\n initiator: request,\n // Clone the mocked response for the \"response\" event listener.\n // This way, the listener can read the response and not lock its body\n // for the actual fetch consumer.\n response: FetchResponse.clone(response),\n responseType: 'mock',\n request,\n requestId,\n })\n )\n }\n\n responsePromise.resolve(response)\n },\n errorWith: (reason) => {\n logger.verbose('request aborted %o', { reason })\n responsePromise.reject(reason)\n },\n },\n {\n logger,\n requestId,\n }\n )\n\n logger.verbose('awaiting request resolution')\n\n logger.verbose(\n 'emitting the \"request\" event for %s listener(s)...',\n this.emitter.listenerCount('request')\n )\n\n /**\n * @note Give the consumer a chance to abort the request before\n * it is dispatched. Fetch queues the request processing as a\n * task, so a signal aborted synchronously after `fetch()` must\n * prevent the request from ever reaching the \"request\" listeners.\n * Without this, the first listener is invoked synchronously\n * within the `fetch()` call itself.\n */\n await Promise.resolve()\n\n await handleRequest({\n initiator: request,\n request,\n requestId,\n emitter: this.emitter,\n controller,\n logger,\n })\n\n return responsePromise.promise\n }\n })\n )\n\n logger.verbose('global fetch patched: %s', globalThis.fetch.name)\n }\n}\n"],"mappings":"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"}
|