@mswjs/interceptors 0.32.1 → 0.33.0

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.
Files changed (121) hide show
  1. package/README.md +35 -7
  2. package/lib/browser/{chunk-732REFPX.mjs → chunk-5ETVT6GU.mjs} +28 -79
  3. package/lib/browser/chunk-5ETVT6GU.mjs.map +1 -0
  4. package/lib/browser/chunk-6MBJUL74.js +142 -0
  5. package/lib/browser/chunk-6MBJUL74.js.map +1 -0
  6. package/lib/browser/chunk-7A4UJNSW.mjs +196 -0
  7. package/lib/browser/chunk-7A4UJNSW.mjs.map +1 -0
  8. package/lib/browser/{chunk-PSX5J3RF.js → chunk-7GVJEW45.js} +30 -81
  9. package/lib/browser/chunk-7GVJEW45.js.map +1 -0
  10. package/lib/browser/{chunk-2CRB3JAQ.js → chunk-FXSPMSSQ.js} +1 -1
  11. package/lib/browser/chunk-FXSPMSSQ.js.map +1 -0
  12. package/lib/browser/{chunk-OMISYKWR.mjs → chunk-GGUENBDN.mjs} +1 -1
  13. package/lib/browser/chunk-GGUENBDN.mjs.map +1 -0
  14. package/lib/browser/chunk-NU2MPFD6.mjs +142 -0
  15. package/lib/browser/chunk-NU2MPFD6.mjs.map +1 -0
  16. package/lib/browser/chunk-VRKVKT62.js +196 -0
  17. package/lib/browser/chunk-VRKVKT62.js.map +1 -0
  18. package/lib/browser/glossary-7d7adb4b.d.ts +66 -0
  19. package/lib/browser/index.d.ts +1 -1
  20. package/lib/browser/index.js +2 -2
  21. package/lib/browser/index.mjs +1 -1
  22. package/lib/browser/interceptors/XMLHttpRequest/index.d.ts +2 -6
  23. package/lib/browser/interceptors/XMLHttpRequest/index.js +4 -4
  24. package/lib/browser/interceptors/XMLHttpRequest/index.mjs +3 -3
  25. package/lib/browser/interceptors/fetch/index.d.ts +1 -1
  26. package/lib/browser/interceptors/fetch/index.js +4 -4
  27. package/lib/browser/interceptors/fetch/index.mjs +3 -3
  28. package/lib/browser/presets/browser.d.ts +1 -1
  29. package/lib/browser/presets/browser.js +6 -6
  30. package/lib/browser/presets/browser.mjs +4 -4
  31. package/lib/node/{BatchInterceptor-2badedde.d.ts → BatchInterceptor-13d40c95.d.ts} +1 -1
  32. package/lib/node/{Interceptor-88ee47c0.d.ts → Interceptor-a31b1217.d.ts} +35 -13
  33. package/lib/node/RemoteHttpInterceptor.d.ts +2 -2
  34. package/lib/node/RemoteHttpInterceptor.js +55 -52
  35. package/lib/node/RemoteHttpInterceptor.js.map +1 -1
  36. package/lib/node/RemoteHttpInterceptor.mjs +53 -50
  37. package/lib/node/RemoteHttpInterceptor.mjs.map +1 -1
  38. package/lib/node/{chunk-5JMJ55U7.js → chunk-2MWIWEWV.js} +144 -113
  39. package/lib/node/chunk-2MWIWEWV.js.map +1 -0
  40. package/lib/node/{chunk-2COJKQQB.js → chunk-42632LKH.js} +3 -3
  41. package/lib/node/chunk-5WWNCLB3.js +196 -0
  42. package/lib/node/chunk-5WWNCLB3.js.map +1 -0
  43. package/lib/node/{chunk-TGTPXCLF.mjs → chunk-BUCULLYM.mjs} +1 -1
  44. package/lib/node/{chunk-TGTPXCLF.mjs.map → chunk-BUCULLYM.mjs.map} +1 -1
  45. package/lib/node/{chunk-OJ6O4LSC.mjs → chunk-BZ3Y7YV5.mjs} +1 -1
  46. package/lib/node/chunk-BZ3Y7YV5.mjs.map +1 -0
  47. package/lib/node/{chunk-3OJLYEWA.mjs → chunk-CU3YXMM4.mjs} +138 -107
  48. package/lib/node/chunk-CU3YXMM4.mjs.map +1 -0
  49. package/lib/node/{chunk-PNWPIDEL.mjs → chunk-HGQLG7KE.mjs} +2 -2
  50. package/lib/node/{chunk-EIBTX65O.js → chunk-IDEEMJ3F.js} +1 -1
  51. package/lib/node/chunk-IDEEMJ3F.js.map +1 -0
  52. package/lib/node/chunk-KY3RJ2M3.mjs +196 -0
  53. package/lib/node/chunk-KY3RJ2M3.mjs.map +1 -0
  54. package/lib/node/{chunk-PYD4E2EJ.js → chunk-P6QG76R3.js} +34 -85
  55. package/lib/node/chunk-P6QG76R3.js.map +1 -0
  56. package/lib/node/{chunk-DV4PBH4D.mjs → chunk-TOV4TYIX.mjs} +29 -80
  57. package/lib/node/chunk-TOV4TYIX.mjs.map +1 -0
  58. package/lib/node/{chunk-BFLYGQ6D.js → chunk-YGM3BCJU.js} +1 -1
  59. package/lib/node/chunk-YGM3BCJU.js.map +1 -0
  60. package/lib/node/index.d.ts +2 -2
  61. package/lib/node/index.js +4 -4
  62. package/lib/node/index.mjs +3 -3
  63. package/lib/node/interceptors/ClientRequest/index.d.ts +2 -2
  64. package/lib/node/interceptors/ClientRequest/index.js +4 -4
  65. package/lib/node/interceptors/ClientRequest/index.mjs +3 -3
  66. package/lib/node/interceptors/XMLHttpRequest/index.d.ts +2 -6
  67. package/lib/node/interceptors/XMLHttpRequest/index.js +5 -5
  68. package/lib/node/interceptors/XMLHttpRequest/index.mjs +4 -4
  69. package/lib/node/interceptors/fetch/index.d.ts +1 -1
  70. package/lib/node/interceptors/fetch/index.js +52 -123
  71. package/lib/node/interceptors/fetch/index.js.map +1 -1
  72. package/lib/node/interceptors/fetch/index.mjs +50 -121
  73. package/lib/node/interceptors/fetch/index.mjs.map +1 -1
  74. package/lib/node/presets/node.d.ts +1 -1
  75. package/lib/node/presets/node.js +7 -7
  76. package/lib/node/presets/node.mjs +5 -5
  77. package/package.json +2 -2
  78. package/src/InterceptorError.ts +7 -0
  79. package/src/RemoteHttpInterceptor.ts +62 -57
  80. package/src/RequestController.test.ts +49 -0
  81. package/src/RequestController.ts +81 -0
  82. package/src/glossary.ts +4 -6
  83. package/src/interceptors/ClientRequest/MockHttpSocket.ts +22 -16
  84. package/src/interceptors/ClientRequest/index.test.ts +2 -33
  85. package/src/interceptors/ClientRequest/index.ts +32 -82
  86. package/src/interceptors/ClientRequest/utils/recordRawHeaders.ts +170 -0
  87. package/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts +1 -1
  88. package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +27 -108
  89. package/src/interceptors/XMLHttpRequest/index.ts +0 -6
  90. package/src/interceptors/fetch/index.ts +52 -169
  91. package/src/utils/handleRequest.ts +213 -0
  92. package/src/utils/responseUtils.ts +4 -4
  93. package/lib/browser/chunk-2CRB3JAQ.js.map +0 -1
  94. package/lib/browser/chunk-732REFPX.mjs.map +0 -1
  95. package/lib/browser/chunk-MAEPOYB6.mjs +0 -213
  96. package/lib/browser/chunk-MAEPOYB6.mjs.map +0 -1
  97. package/lib/browser/chunk-MQJ3JOOK.js +0 -49
  98. package/lib/browser/chunk-MQJ3JOOK.js.map +0 -1
  99. package/lib/browser/chunk-OMISYKWR.mjs.map +0 -1
  100. package/lib/browser/chunk-OUWBQF3Z.mjs +0 -49
  101. package/lib/browser/chunk-OUWBQF3Z.mjs.map +0 -1
  102. package/lib/browser/chunk-PSX5J3RF.js.map +0 -1
  103. package/lib/browser/chunk-WBHIW62P.js +0 -213
  104. package/lib/browser/chunk-WBHIW62P.js.map +0 -1
  105. package/lib/browser/glossary-1c204f45.d.ts +0 -44
  106. package/lib/node/chunk-3OJLYEWA.mjs.map +0 -1
  107. package/lib/node/chunk-5JMJ55U7.js.map +0 -1
  108. package/lib/node/chunk-BFLYGQ6D.js.map +0 -1
  109. package/lib/node/chunk-DV4PBH4D.mjs.map +0 -1
  110. package/lib/node/chunk-EIBTX65O.js.map +0 -1
  111. package/lib/node/chunk-KWV3JXSI.mjs +0 -49
  112. package/lib/node/chunk-KWV3JXSI.mjs.map +0 -1
  113. package/lib/node/chunk-OJ6O4LSC.mjs.map +0 -1
  114. package/lib/node/chunk-PYD4E2EJ.js.map +0 -1
  115. package/lib/node/chunk-UXCYRE4F.js +0 -49
  116. package/lib/node/chunk-UXCYRE4F.js.map +0 -1
  117. package/src/utils/getRawFetchHeaders.test.ts +0 -50
  118. package/src/utils/getRawFetchHeaders.ts +0 -56
  119. package/src/utils/toInteractiveRequest.ts +0 -23
  120. /package/lib/node/{chunk-2COJKQQB.js.map → chunk-42632LKH.js.map} +0 -0
  121. /package/lib/node/{chunk-PNWPIDEL.mjs.map → chunk-HGQLG7KE.mjs.map} +0 -0
@@ -0,0 +1,170 @@
1
+ type HeaderTuple = [string, string]
2
+ type RawHeaders = Array<HeaderTuple>
3
+
4
+ const kRawHeaders = Symbol('kRawHeaders')
5
+ const kRestorePatches = Symbol('kRestorePatches')
6
+
7
+ function recordRawHeader(headers: Headers, args: HeaderTuple) {
8
+ if (Reflect.get(headers, kRawHeaders) == null) {
9
+ Object.defineProperty(headers, kRawHeaders, {
10
+ value: [],
11
+ enumerable: false,
12
+ })
13
+ }
14
+ const rawHeaders = Reflect.get(headers, kRawHeaders) as RawHeaders
15
+ rawHeaders.push(args)
16
+ }
17
+
18
+ /**
19
+ * Patch the global `Headers` class to store raw headers.
20
+ * This is for compatibility with `IncomingMessage.prototype.rawHeaders`.
21
+ *
22
+ * @note Node.js has their own raw headers symbol but it
23
+ * only records the first header name in case of multi-value headers.
24
+ * Any other headers are normalized before comparing. This makes it
25
+ * incompatible with the `rawHeaders` format.
26
+ *
27
+ * let h = new Headers()
28
+ * h.append('X-Custom', 'one')
29
+ * h.append('x-custom', 'two')
30
+ * h[Symbol('headers map')] // Map { 'X-Custom' => 'one, two' }
31
+ */
32
+ export function recordRawFetchHeaders() {
33
+ // Prevent patching the Headers prototype multiple times.
34
+ if (Reflect.get(Headers, kRestorePatches)) {
35
+ return Reflect.get(Headers, kRestorePatches)
36
+ }
37
+
38
+ const { Request: OriginalRequest, Response: OriginalResponse } = globalThis
39
+ const { set, append, delete: headersDeleteMethod } = Headers.prototype
40
+
41
+ Object.defineProperty(Headers, kRestorePatches, {
42
+ value: () => {
43
+ Headers.prototype.set = set
44
+ Headers.prototype.append = append
45
+ Headers.prototype.delete = headersDeleteMethod
46
+
47
+ globalThis.Request = OriginalRequest
48
+ globalThis.Response = OriginalResponse
49
+ },
50
+ enumerable: false,
51
+ })
52
+
53
+ Headers = new Proxy(Headers, {
54
+ construct(target, args, newTarget) {
55
+ const headers = Reflect.construct(target, args, newTarget)
56
+ const initialHeaders = args[0] || []
57
+ const initialRawHeaders = Array.isArray(initialHeaders)
58
+ ? initialHeaders
59
+ : Object.entries(initialHeaders)
60
+
61
+ // Request/Response constructors will set the symbol
62
+ // upon creating a new instance, using the raw developer
63
+ // input as the raw headers. Skip the symbol altogether
64
+ // in those cases because the input to Headers will be normalized.
65
+ if (!Reflect.has(headers, kRawHeaders)) {
66
+ Object.defineProperty(headers, kRawHeaders, {
67
+ value: initialRawHeaders,
68
+ enumerable: false,
69
+ })
70
+ }
71
+
72
+ return headers
73
+ },
74
+ })
75
+
76
+ Headers.prototype.set = new Proxy(Headers.prototype.set, {
77
+ apply(target, thisArg, args: HeaderTuple) {
78
+ recordRawHeader(thisArg, args)
79
+ return Reflect.apply(target, thisArg, args)
80
+ },
81
+ })
82
+
83
+ Headers.prototype.append = new Proxy(Headers.prototype.append, {
84
+ apply(target, thisArg, args: HeaderTuple) {
85
+ recordRawHeader(thisArg, args)
86
+ return Reflect.apply(target, thisArg, args)
87
+ },
88
+ })
89
+
90
+ Headers.prototype.delete = new Proxy(Headers.prototype.delete, {
91
+ apply(target, thisArg, args: [string]) {
92
+ const rawHeaders = Reflect.get(thisArg, kRawHeaders) as RawHeaders
93
+
94
+ if (rawHeaders) {
95
+ for (let index = rawHeaders.length - 1; index >= 0; index--) {
96
+ if (rawHeaders[index][0].toLowerCase() === args[0].toLowerCase()) {
97
+ rawHeaders.splice(index, 1)
98
+ }
99
+ }
100
+ }
101
+
102
+ return Reflect.apply(target, thisArg, args)
103
+ },
104
+ })
105
+
106
+ Request = new Proxy(Request, {
107
+ construct(target, args, newTarget) {
108
+ const request = Reflect.construct(target, args, newTarget)
109
+
110
+ if (
111
+ typeof args[1] === 'object' &&
112
+ args[1].headers != null &&
113
+ !request.headers[kRawHeaders]
114
+ ) {
115
+ request.headers[kRawHeaders] = inferRawHeaders(args[1].headers)
116
+ }
117
+
118
+ return request
119
+ },
120
+ })
121
+
122
+ Response = new Proxy(Response, {
123
+ construct(target, args, newTarget) {
124
+ const response = Reflect.construct(target, args, newTarget)
125
+
126
+ if (typeof args[1] === 'object' && args[1].headers != null) {
127
+ /**
128
+ * @note Pass the init argument directly because it gets
129
+ * transformed into a normalized Headers instance once it
130
+ * passes the Response constructor.
131
+ */
132
+ response.headers[kRawHeaders] = inferRawHeaders(args[1].headers)
133
+ }
134
+
135
+ return response
136
+ },
137
+ })
138
+ }
139
+
140
+ export function restoreHeadersPrototype() {
141
+ if (!Reflect.get(Headers, kRestorePatches)) {
142
+ return
143
+ }
144
+
145
+ Reflect.get(Headers, kRestorePatches)()
146
+ }
147
+
148
+ export function getRawFetchHeaders(headers: Headers): RawHeaders {
149
+ // Return the raw headers, if recorded (i.e. `.set()` or `.append()` was called).
150
+ // If no raw headers were recorded, return all the headers.
151
+ return Reflect.get(headers, kRawHeaders) || Array.from(headers.entries())
152
+ }
153
+
154
+ /**
155
+ * Infers the raw headers from the given `HeadersInit` provided
156
+ * to the Request/Response constructor.
157
+ *
158
+ * If the `init.headers` is a Headers instance, use it directly.
159
+ * That means the headers were created standalone and already have
160
+ * the raw headers stored.
161
+ * If the `init.headers` is a HeadersInit, create a new Headers
162
+ * instace out of it.
163
+ */
164
+ function inferRawHeaders(headers: HeadersInit): RawHeaders {
165
+ if (headers instanceof Headers) {
166
+ return Reflect.get(headers, kRawHeaders)
167
+ }
168
+
169
+ return Reflect.get(new Headers(headers), kRawHeaders)
170
+ }
@@ -481,7 +481,7 @@ export class XMLHttpRequestController {
481
481
  return null
482
482
  }
483
483
 
484
- public errorWith(error: Error): void {
484
+ public errorWith(error?: Error): void {
485
485
  this.logger.info('responding with an error')
486
486
 
487
487
  this.setReadyState(this.request.DONE)
@@ -1,13 +1,8 @@
1
- import { until } from '@open-draft/until'
2
1
  import type { Logger } from '@open-draft/logger'
3
2
  import { XMLHttpRequestEmitter } from '.'
4
- import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
5
- import { emitAsync } from '../../utils/emitAsync'
3
+ import { RequestController } from '../../RequestController'
6
4
  import { XMLHttpRequestController } from './XMLHttpRequestController'
7
- import {
8
- createServerErrorResponse,
9
- isResponseError,
10
- } from '../../utils/responseUtils'
5
+ import { handleRequest } from '../../utils/handleRequest'
11
6
 
12
7
  export interface XMLHttpRequestProxyOptions {
13
8
  emitter: XMLHttpRequestEmitter
@@ -57,116 +52,40 @@ export function createXMLHttpRequestProxy({
57
52
  )
58
53
 
59
54
  xhrRequestController.onRequest = async function ({ request, requestId }) {
60
- const { interactiveRequest, requestController } =
61
- toInteractiveRequest(request)
55
+ const controller = new RequestController(request)
62
56
 
63
57
  this.logger.info('awaiting mocked response...')
64
58
 
65
- emitter.once('request', ({ requestId: pendingRequestId }) => {
66
- if (pendingRequestId !== requestId) {
67
- return
68
- }
69
-
70
- if (requestController.responsePromise.state === 'pending') {
71
- requestController.respondWith(undefined)
72
- }
73
- })
74
-
75
- const resolverResult = await until(async () => {
76
- this.logger.info(
77
- 'emitting the "request" event for %s listener(s)...',
78
- emitter.listenerCount('request')
79
- )
80
-
81
- await emitAsync(emitter, 'request', {
82
- request: interactiveRequest,
83
- requestId,
84
- })
85
-
86
- this.logger.info('all "request" listeners settled!')
87
-
88
- const mockedResponse = await requestController.responsePromise
89
-
90
- this.logger.info('event.respondWith called with:', mockedResponse)
91
-
92
- return mockedResponse
93
- })
94
-
95
- if (resolverResult.error) {
96
- this.logger.info(
97
- 'request listener threw an exception, aborting request...',
98
- resolverResult.error
99
- )
100
-
101
- // Treat thrown Responses as mocked responses.
102
- if (resolverResult.error instanceof Response) {
103
- if (isResponseError(resolverResult.error)) {
104
- xhrRequestController.errorWith(new TypeError('Network error'))
105
- } else {
106
- this.respondWith(resolverResult.error)
107
- }
108
-
109
- return
110
- }
111
-
112
- if (emitter.listenerCount('unhandledException') > 0) {
113
- // Emit the "unhandledException" event so the client can opt-out
114
- // from the default exception handling (producing 500 error responses).
115
- await emitAsync(emitter, 'unhandledException', {
116
- error: resolverResult.error,
117
- request,
118
- requestId,
119
- controller: {
120
- respondWith:
121
- xhrRequestController.respondWith.bind(xhrRequestController),
122
- errorWith:
123
- xhrRequestController.errorWith.bind(xhrRequestController),
124
- },
125
- })
59
+ this.logger.info(
60
+ 'emitting the "request" event for %s listener(s)...',
61
+ emitter.listenerCount('request')
62
+ )
126
63
 
127
- // If any of the "unhandledException" listeners handled the request,
128
- // do nothing. Note that mocked responses will dispatch
129
- // HEADERS_RECEIVED (2), then LOADING (3), and DONE (4) can take
130
- // time as the mocked response body finishes streaming.
131
- if (originalRequest.readyState > XMLHttpRequest.OPENED) {
132
- return
64
+ const isRequestHandled = await handleRequest({
65
+ request,
66
+ requestId,
67
+ controller,
68
+ emitter,
69
+ onResponse: (response) => {
70
+ this.respondWith(response)
71
+ },
72
+ onRequestError: () => {
73
+ this.errorWith(new TypeError('Network error'))
74
+ },
75
+ onError: (error) => {
76
+ this.logger.info('request errored!', { error })
77
+
78
+ if (error instanceof Error) {
79
+ this.errorWith(error)
133
80
  }
134
- }
135
-
136
- // Unhandled exceptions in the request listeners are
137
- // synonymous to unhandled exceptions on the server.
138
- // Those are represented as 500 error responses.
139
- xhrRequestController.respondWith(
140
- createServerErrorResponse(resolverResult.error)
141
- )
142
-
143
- return
144
- }
145
-
146
- const mockedResponse = resolverResult.data
81
+ },
82
+ })
147
83
 
148
- if (typeof mockedResponse !== 'undefined') {
84
+ if (!isRequestHandled) {
149
85
  this.logger.info(
150
- 'received mocked response: %d %s',
151
- mockedResponse.status,
152
- mockedResponse.statusText
86
+ 'no mocked response received, performing request as-is...'
153
87
  )
154
-
155
- if (isResponseError(mockedResponse)) {
156
- this.logger.info(
157
- 'received a network error response, rejecting the request promise...'
158
- )
159
-
160
- xhrRequestController.errorWith(new TypeError('Network error'))
161
- return
162
- }
163
-
164
- return xhrRequestController.respondWith(mockedResponse)
165
88
  }
166
-
167
- this.logger.info(
168
- 'no mocked response received, performing request as-is...'
169
- )
170
89
  }
171
90
 
172
91
  xhrRequestController.onResponse = async function ({
@@ -1,15 +1,9 @@
1
1
  import { invariant } from 'outvariant'
2
2
  import { Emitter } from 'strict-event-emitter'
3
3
  import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
4
- import { InteractiveRequest } from '../../utils/toInteractiveRequest'
5
4
  import { Interceptor } from '../../Interceptor'
6
5
  import { createXMLHttpRequestProxy } from './XMLHttpRequestProxy'
7
6
 
8
- export type XMLHttpRequestEventListener = (args: {
9
- request: InteractiveRequest
10
- requestId: string
11
- }) => Promise<void> | void
12
-
13
7
  export type XMLHttpRequestEmitter = Emitter<HttpRequestEventMap>
14
8
 
15
9
  export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap> {
@@ -1,16 +1,12 @@
1
1
  import { invariant } from 'outvariant'
2
2
  import { DeferredPromise } from '@open-draft/deferred-promise'
3
- import { until } from '@open-draft/until'
4
3
  import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
5
4
  import { Interceptor } from '../../Interceptor'
6
- import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
5
+ import { RequestController } from '../../RequestController'
7
6
  import { emitAsync } from '../../utils/emitAsync'
7
+ import { handleRequest } from '../../utils/handleRequest'
8
8
  import { canParseUrl } from '../../utils/canParseUrl'
9
9
  import { createRequestId } from '../../createRequestId'
10
- import {
11
- createServerErrorResponse,
12
- isResponseError,
13
- } from '../../utils/responseUtils'
14
10
 
15
11
  export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
16
12
  static symbol = Symbol('fetch')
@@ -51,185 +47,72 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
51
47
  : input
52
48
 
53
49
  const request = new Request(resolvedInput, init)
50
+ const responsePromise = new DeferredPromise<Response>()
51
+ const controller = new RequestController(request)
54
52
 
55
53
  this.logger.info('[%s] %s', request.method, request.url)
56
-
57
- const { interactiveRequest, requestController } =
58
- toInteractiveRequest(request)
54
+ this.logger.info('awaiting for the mocked response...')
59
55
 
60
56
  this.logger.info(
61
- 'emitting the "request" event for %d listener(s)...',
57
+ 'emitting the "request" event for %s listener(s)...',
62
58
  this.emitter.listenerCount('request')
63
59
  )
64
60
 
65
- this.emitter.once('request', ({ requestId: pendingRequestId }) => {
66
- if (pendingRequestId !== requestId) {
67
- return
68
- }
69
-
70
- if (requestController.responsePromise.state === 'pending') {
71
- requestController.responsePromise.resolve(undefined)
72
- }
73
- })
74
-
75
- this.logger.info('awaiting for the mocked response...')
76
-
77
- const signal = interactiveRequest.signal
78
- const requestAborted = new DeferredPromise()
79
-
80
- // Signal isn't always defined in react-native.
81
- if (signal) {
82
- signal.addEventListener(
83
- 'abort',
84
- () => {
85
- requestAborted.reject(signal.reason)
86
- },
87
- { once: true }
88
- )
89
- }
90
-
91
- const responsePromise = new DeferredPromise<Response>()
92
-
93
- const respondWith = (response: Response): void => {
94
- this.logger.info('responding with a mock response:', response)
95
-
96
- if (this.emitter.listenerCount('response') > 0) {
97
- this.logger.info('emitting the "response" event...')
98
-
99
- // Clone the mocked response for the "response" event listener.
100
- // This way, the listener can read the response and not lock its body
101
- // for the actual fetch consumer.
102
- const responseClone = response.clone()
103
-
104
- this.emitter.emit('response', {
105
- response: responseClone,
106
- isMockedResponse: true,
107
- request: interactiveRequest,
108
- requestId,
109
- })
110
- }
111
-
112
- // Set the "response.url" property to equal the intercepted request URL.
113
- Object.defineProperty(response, 'url', {
114
- writable: false,
115
- enumerable: true,
116
- configurable: false,
117
- value: request.url,
118
- })
119
-
120
- responsePromise.resolve(response)
121
- }
122
-
123
- const errorWith = (reason: unknown): void => {
124
- responsePromise.reject(reason)
125
- }
126
-
127
- const resolverResult = await until<unknown, Response | undefined>(
128
- async () => {
129
- const listenersFinished = emitAsync(this.emitter, 'request', {
130
- request: interactiveRequest,
131
- requestId,
61
+ const isRequestHandled = await handleRequest({
62
+ request,
63
+ requestId,
64
+ emitter: this.emitter,
65
+ controller,
66
+ onResponse: async (response) => {
67
+ this.logger.info('received mocked response!', {
68
+ response,
132
69
  })
133
70
 
134
- await Promise.race([
135
- requestAborted,
136
- // Put the listeners invocation Promise in the same race condition
137
- // with the request abort Promise because otherwise awaiting the listeners
138
- // would always yield some response (or undefined).
139
- listenersFinished,
140
- requestController.responsePromise,
141
- ])
142
-
143
- this.logger.info('all request listeners have been resolved!')
144
-
145
- const mockedResponse = await requestController.responsePromise
146
- this.logger.info('event.respondWith called with:', mockedResponse)
147
-
148
- return mockedResponse
149
- }
150
- )
151
-
152
- if (requestAborted.state === 'rejected') {
153
- this.logger.info(
154
- 'request has been aborted:',
155
- requestAborted.rejectionReason
156
- )
157
-
158
- responsePromise.reject(requestAborted.rejectionReason)
159
- return responsePromise
160
- }
161
-
162
- if (resolverResult.error) {
163
- this.logger.info(
164
- 'request listerner threw an error:',
165
- resolverResult.error
166
- )
167
-
168
- // Treat thrown Responses as mocked responses.
169
- if (resolverResult.error instanceof Response) {
170
- // Treat thrown Response.error() as a request error.
171
- if (isResponseError(resolverResult.error)) {
172
- errorWith(createNetworkError(resolverResult.error))
173
- } else {
174
- // Treat the rest of thrown Responses as mocked responses.
175
- respondWith(resolverResult.error)
71
+ if (this.emitter.listenerCount('response') > 0) {
72
+ this.logger.info('emitting the "response" event...')
73
+
74
+ // Await the response listeners to finish before resolving
75
+ // the response promise. This ensures all your logic finishes
76
+ // before the interceptor resolves the pending response.
77
+ await emitAsync(this.emitter, 'response', {
78
+ // Clone the mocked response for the "response" event listener.
79
+ // This way, the listener can read the response and not lock its body
80
+ // for the actual fetch consumer.
81
+ response: response.clone(),
82
+ isMockedResponse: true,
83
+ request,
84
+ requestId,
85
+ })
176
86
  }
177
- }
178
87
 
179
- // Emit the "unhandledException" interceptor event so the client
180
- // can opt-out from exceptions translating to 500 error responses.
181
-
182
- if (this.emitter.listenerCount('unhandledException') > 0) {
183
- await emitAsync(this.emitter, 'unhandledException', {
184
- error: resolverResult.error,
185
- request,
186
- requestId,
187
- controller: {
188
- respondWith,
189
- errorWith,
190
- },
88
+ // Set the "response.url" property to equal the intercepted request URL.
89
+ Object.defineProperty(response, 'url', {
90
+ writable: false,
91
+ enumerable: true,
92
+ configurable: false,
93
+ value: request.url,
191
94
  })
192
95
 
193
- if (responsePromise.state !== 'pending') {
194
- return responsePromise
195
- }
196
- }
197
-
198
- // Unhandled exceptions in the request listeners are
199
- // synonymous to unhandled exceptions on the server.
200
- // Those are represented as 500 error responses.
201
- respondWith(createServerErrorResponse(resolverResult.error))
202
- return responsePromise
203
- }
204
-
205
- const mockedResponse = resolverResult.data
206
-
207
- if (mockedResponse && !request.signal?.aborted) {
208
- this.logger.info('received mocked response:', mockedResponse)
209
-
210
- // Reject the request Promise on mocked "Response.error" responses.
211
- if (isResponseError(mockedResponse)) {
212
- this.logger.info(
213
- 'received a network error response, rejecting the request promise...'
214
- )
215
-
216
- /**
217
- * Set the cause of the request promise rejection to the
218
- * network error Response instance. This differs from Undici.
219
- * Undici will forward the "response.error" custom property
220
- * as the rejection reason but for "Response.error()" static method
221
- * "response.error" will equal to undefined, making "cause" an empty Error.
222
- * @see https://github.com/nodejs/undici/blob/83cb522ae0157a19d149d72c7d03d46e34510d0a/lib/fetch/response.js#L344
223
- */
224
- errorWith(createNetworkError(mockedResponse))
225
- } else {
226
- respondWith(mockedResponse)
227
- }
96
+ responsePromise.resolve(response)
97
+ },
98
+ onRequestError: (response) => {
99
+ this.logger.info('request has errored!', { response })
100
+ responsePromise.reject(createNetworkError(response))
101
+ },
102
+ onError: (error) => {
103
+ this.logger.info('request has been aborted!', { error })
104
+ responsePromise.reject(error)
105
+ },
106
+ })
228
107
 
108
+ if (isRequestHandled) {
109
+ this.logger.info('request has been handled, returning mock promise...')
229
110
  return responsePromise
230
111
  }
231
112
 
232
- this.logger.info('no mocked response received!')
113
+ this.logger.info(
114
+ 'no mocked response received, performing request as-is...'
115
+ )
233
116
 
234
117
  return pureFetch(request).then((response) => {
235
118
  this.logger.info('original fetch performed', response)
@@ -242,7 +125,7 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
242
125
  this.emitter.emit('response', {
243
126
  response: responseClone,
244
127
  isMockedResponse: false,
245
- request: interactiveRequest,
128
+ request,
246
129
  requestId,
247
130
  })
248
131
  }