@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.
@@ -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
- socketController.decline()
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 Only inspect the first sent packet to determine the protocol.
116
- * A single socket cannot be used for different protocols.
143
+ * @note Inspect the first sent packet to determine the protocol,
144
+ * including when entering a mocked "CONNECT" tunnel.
117
145
  */
118
- socket.on('data', (chunk) => {
146
+ const onRequestData = (chunk: Buffer) => {
119
147
  if (isHttpConnection === false) {
120
- socketController.decline()
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 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.
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.execute(toBuffer(chunk))
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
- // Decline non-HTTP connections so the socket controller can
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
- socketController.decline()
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 response = FetchResponse.from(rawResponse, {
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 onResponseClose = () => {
426
+ const onResponseEnd = () => {
406
427
  disposeResponseParser()
407
428
 
408
- // Without a response, no response listener will release
409
- // the buffered EOF/close that rejects the client request.
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('close', onResponseClose)
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('close', onResponseClose)
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.execute(toBuffer(chunk))
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
- * @note Expect a verdict on this connection from every
281
- * "connection" listener before emitting the event. With no
282
- * listeners to claim the connection (or once every listener
283
- * declines it), the controller passes it through as-is.
284
- */
285
- controller.awaitVerdicts(interceptor.listenerCount('connection'))
286
-
287
- interceptor.emitter.emit(
288
- new SocketConnectionEvent({
289
- socket: controller.serverSocket,
290
- controller,
291
- connectionOptions,
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: false,
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. The verdict count is deliberately not re-armed: subscribers
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.#passthroughSocket
622
- .removeListener('connect', this.#onRealSocketConnect)
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
- if (realSocket !== this.#passthroughSocket) {
1342
- this.#passthroughSocket = realSocket
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
- realSocket
1382
- .removeListener('connect', this.#onRealSocketConnect)
1383
- .removeListener(
1384
- 'connectionAttemptFailed',
1385
- this.#onRealSocketConnectionAttemptFailed
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
- realSocket
1397
- .once('connect', this.#onRealSocketConnect)
1398
- .on('connectionAttemptFailed', this.#onRealSocketConnectionAttemptFailed)
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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"}