@mswjs/interceptors 0.39.8 → 0.40.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 (133) hide show
  1. package/lib/browser/{chunk-E3CCOBRX.js → chunk-2MCNQOY3.js} +54 -49
  2. package/lib/browser/chunk-2MCNQOY3.js.map +1 -0
  3. package/lib/browser/chunk-57RIRQUY.js +218 -0
  4. package/lib/browser/chunk-57RIRQUY.js.map +1 -0
  5. package/lib/browser/chunk-FW45TRCB.js +178 -0
  6. package/lib/browser/chunk-FW45TRCB.js.map +1 -0
  7. package/lib/browser/{chunk-TIPR373R.js → chunk-JQ2S7G56.js} +19 -3
  8. package/lib/browser/chunk-JQ2S7G56.js.map +1 -0
  9. package/lib/browser/{chunk-3RXCRGL2.mjs → chunk-LIKZF2VU.mjs} +102 -1
  10. package/lib/browser/chunk-LIKZF2VU.mjs.map +1 -0
  11. package/lib/browser/{chunk-H74PGQ4Y.js → chunk-MNT2FUCH.js} +58 -53
  12. package/lib/browser/chunk-MNT2FUCH.js.map +1 -0
  13. package/lib/browser/chunk-VOUOVDAW.mjs +178 -0
  14. package/lib/browser/chunk-VOUOVDAW.mjs.map +1 -0
  15. package/lib/browser/{chunk-E7UVBHVO.mjs → chunk-WADP6VHN.mjs} +48 -43
  16. package/lib/browser/chunk-WADP6VHN.mjs.map +1 -0
  17. package/lib/browser/{chunk-Q7K2XAEP.mjs → chunk-WOWPV4GR.mjs} +50 -45
  18. package/lib/browser/chunk-WOWPV4GR.mjs.map +1 -0
  19. package/lib/browser/{chunk-QED3Q6Z2.mjs → chunk-Z5TSB3T6.mjs} +17 -1
  20. package/lib/browser/{chunk-QED3Q6Z2.mjs.map → chunk-Z5TSB3T6.mjs.map} +1 -1
  21. package/lib/browser/{glossary-7152281e.d.ts → glossary-f7ee1c9d.d.ts} +22 -17
  22. package/lib/browser/index.d.ts +1 -2
  23. package/lib/browser/index.js +6 -4
  24. package/lib/browser/index.js.map +1 -1
  25. package/lib/browser/index.mjs +4 -2
  26. package/lib/browser/index.mjs.map +1 -1
  27. package/lib/browser/interceptors/WebSocket/index.js +3 -3
  28. package/lib/browser/interceptors/WebSocket/index.mjs +1 -1
  29. package/lib/browser/interceptors/XMLHttpRequest/index.d.ts +1 -2
  30. package/lib/browser/interceptors/XMLHttpRequest/index.js +5 -5
  31. package/lib/browser/interceptors/XMLHttpRequest/index.mjs +4 -4
  32. package/lib/browser/interceptors/fetch/index.d.ts +1 -2
  33. package/lib/browser/interceptors/fetch/index.js +5 -5
  34. package/lib/browser/interceptors/fetch/index.mjs +4 -4
  35. package/lib/browser/presets/browser.d.ts +1 -2
  36. package/lib/browser/presets/browser.js +7 -7
  37. package/lib/browser/presets/browser.mjs +5 -5
  38. package/lib/node/{BatchInterceptor-5b72232f.d.ts → BatchInterceptor-cb9a2eee.d.ts} +1 -1
  39. package/lib/node/{Interceptor-bc5a9d8e.d.ts → Interceptor-dc0a39b5.d.ts} +22 -16
  40. package/lib/node/RemoteHttpInterceptor.d.ts +2 -3
  41. package/lib/node/RemoteHttpInterceptor.js +31 -27
  42. package/lib/node/RemoteHttpInterceptor.js.map +1 -1
  43. package/lib/node/RemoteHttpInterceptor.mjs +28 -24
  44. package/lib/node/RemoteHttpInterceptor.mjs.map +1 -1
  45. package/lib/node/{chunk-EKNRB5ZS.mjs → chunk-5UGIB6OX.mjs} +40 -29
  46. package/lib/node/chunk-5UGIB6OX.mjs.map +1 -0
  47. package/lib/node/{chunk-4NEYTVWD.mjs → chunk-5V3SIIW2.mjs} +48 -43
  48. package/lib/node/chunk-5V3SIIW2.mjs.map +1 -0
  49. package/lib/node/{chunk-VV2LUF5K.js → chunk-6B3ZQOO2.js} +51 -46
  50. package/lib/node/chunk-6B3ZQOO2.js.map +1 -0
  51. package/lib/node/chunk-7Q53NNPV.js +189 -0
  52. package/lib/node/chunk-7Q53NNPV.js.map +1 -0
  53. package/lib/node/{chunk-A7U44ARP.js → chunk-DOWWQYXZ.js} +104 -3
  54. package/lib/node/chunk-DOWWQYXZ.js.map +1 -0
  55. package/lib/node/{chunk-Z5LWCBZS.js → chunk-FRZQJNBO.js} +56 -51
  56. package/lib/node/chunk-FRZQJNBO.js.map +1 -0
  57. package/lib/node/{chunk-TJDMZZXE.mjs → chunk-GKN5RBVR.mjs} +2 -2
  58. package/lib/node/{chunk-R6JVCM7X.js → chunk-J5MULIHT.js} +3 -3
  59. package/lib/node/{chunk-IHJSPMYM.mjs → chunk-JXGB54LE.mjs} +102 -1
  60. package/lib/node/chunk-JXGB54LE.mjs.map +1 -0
  61. package/lib/node/{chunk-3CNGDJFB.mjs → chunk-OFW5L5ET.mjs} +50 -45
  62. package/lib/node/chunk-OFW5L5ET.mjs.map +1 -0
  63. package/lib/node/{chunk-A7Q4RTDJ.mjs → chunk-R6T7CL5E.mjs} +55 -115
  64. package/lib/node/chunk-R6T7CL5E.mjs.map +1 -0
  65. package/lib/node/{chunk-RC2XPCC4.mjs → chunk-SQ6RHTJR.mjs} +2 -2
  66. package/lib/node/chunk-SRMAQGPM.js +30 -0
  67. package/lib/node/chunk-SRMAQGPM.js.map +1 -0
  68. package/lib/node/{chunk-4YBV77DG.js → chunk-T3TW4P64.js} +3 -3
  69. package/lib/node/{chunk-N4ZZFE24.js → chunk-VYO5XDY2.js} +56 -45
  70. package/lib/node/chunk-VYO5XDY2.js.map +1 -0
  71. package/lib/node/chunk-YWNGXXUQ.mjs +30 -0
  72. package/lib/node/{chunk-3GJB4JDF.mjs.map → chunk-YWNGXXUQ.mjs.map} +1 -1
  73. package/lib/node/index.d.ts +2 -3
  74. package/lib/node/index.js +6 -4
  75. package/lib/node/index.js.map +1 -1
  76. package/lib/node/index.mjs +5 -3
  77. package/lib/node/index.mjs.map +1 -1
  78. package/lib/node/interceptors/ClientRequest/index.d.ts +1 -2
  79. package/lib/node/interceptors/ClientRequest/index.js +6 -6
  80. package/lib/node/interceptors/ClientRequest/index.mjs +5 -5
  81. package/lib/node/interceptors/XMLHttpRequest/index.d.ts +1 -2
  82. package/lib/node/interceptors/XMLHttpRequest/index.js +5 -5
  83. package/lib/node/interceptors/XMLHttpRequest/index.mjs +4 -4
  84. package/lib/node/interceptors/fetch/index.d.ts +1 -2
  85. package/lib/node/interceptors/fetch/index.js +5 -5
  86. package/lib/node/interceptors/fetch/index.mjs +4 -4
  87. package/lib/node/presets/node.d.ts +1 -2
  88. package/lib/node/presets/node.js +10 -10
  89. package/lib/node/presets/node.mjs +7 -7
  90. package/lib/node/utils/node/index.js +3 -3
  91. package/lib/node/utils/node/index.mjs +2 -2
  92. package/package.json +2 -1
  93. package/src/RemoteHttpInterceptor.ts +18 -13
  94. package/src/RequestController.test.ts +78 -31
  95. package/src/RequestController.ts +63 -39
  96. package/src/index.ts +4 -0
  97. package/src/interceptors/ClientRequest/MockHttpSocket.ts +24 -3
  98. package/src/interceptors/ClientRequest/index.ts +14 -18
  99. package/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts +45 -35
  100. package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +24 -21
  101. package/src/interceptors/fetch/index.ts +61 -50
  102. package/src/utils/handleRequest.ts +65 -95
  103. package/lib/browser/chunk-2QICSCCS.js +0 -238
  104. package/lib/browser/chunk-2QICSCCS.js.map +0 -1
  105. package/lib/browser/chunk-3RXCRGL2.mjs.map +0 -1
  106. package/lib/browser/chunk-E3CCOBRX.js.map +0 -1
  107. package/lib/browser/chunk-E7UVBHVO.mjs.map +0 -1
  108. package/lib/browser/chunk-H74PGQ4Y.js.map +0 -1
  109. package/lib/browser/chunk-PTTUYYVR.mjs +0 -238
  110. package/lib/browser/chunk-PTTUYYVR.mjs.map +0 -1
  111. package/lib/browser/chunk-Q7K2XAEP.mjs.map +0 -1
  112. package/lib/browser/chunk-T7TBRNJZ.js +0 -117
  113. package/lib/browser/chunk-T7TBRNJZ.js.map +0 -1
  114. package/lib/browser/chunk-TIPR373R.js.map +0 -1
  115. package/lib/node/chunk-3CNGDJFB.mjs.map +0 -1
  116. package/lib/node/chunk-3GJB4JDF.mjs +0 -14
  117. package/lib/node/chunk-4NEYTVWD.mjs.map +0 -1
  118. package/lib/node/chunk-72ZIHMEB.js +0 -249
  119. package/lib/node/chunk-72ZIHMEB.js.map +0 -1
  120. package/lib/node/chunk-A7Q4RTDJ.mjs.map +0 -1
  121. package/lib/node/chunk-A7U44ARP.js.map +0 -1
  122. package/lib/node/chunk-EKNRB5ZS.mjs.map +0 -1
  123. package/lib/node/chunk-IHJSPMYM.mjs.map +0 -1
  124. package/lib/node/chunk-N4ZZFE24.js.map +0 -1
  125. package/lib/node/chunk-SMXZPJEA.js +0 -14
  126. package/lib/node/chunk-SMXZPJEA.js.map +0 -1
  127. package/lib/node/chunk-VV2LUF5K.js.map +0 -1
  128. package/lib/node/chunk-Z5LWCBZS.js.map +0 -1
  129. package/src/utils/RequestController.ts +0 -21
  130. /package/lib/node/{chunk-TJDMZZXE.mjs.map → chunk-GKN5RBVR.mjs.map} +0 -0
  131. /package/lib/node/{chunk-R6JVCM7X.js.map → chunk-J5MULIHT.js.map} +0 -0
  132. /package/lib/node/{chunk-RC2XPCC4.mjs.map → chunk-SQ6RHTJR.mjs.map} +0 -0
  133. /package/lib/node/{chunk-4YBV77DG.js.map → chunk-T3TW4P64.js.map} +0 -0
@@ -153,30 +153,26 @@ export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
153
153
  request,
154
154
  socket,
155
155
  }) => {
156
- const requestId = Reflect.get(request, kRequestId)
157
- const controller = new RequestController(request)
158
-
159
- const isRequestHandled = await handleRequest({
160
- request,
161
- requestId,
162
- controller,
163
- emitter: this.emitter,
164
- onResponse: (response) => {
165
- socket.respondWith(response)
156
+ const controller = new RequestController(request, {
157
+ passthrough() {
158
+ socket.passthrough()
166
159
  },
167
- onRequestError: (response) => {
168
- socket.respondWith(response)
160
+ async respondWith(response) {
161
+ await socket.respondWith(response)
169
162
  },
170
- onError: (error) => {
171
- if (error instanceof Error) {
172
- socket.errorWith(error)
163
+ errorWith(reason) {
164
+ if (reason instanceof Error) {
165
+ socket.errorWith(reason)
173
166
  }
174
167
  },
175
168
  })
176
169
 
177
- if (!isRequestHandled) {
178
- return socket.passthrough()
179
- }
170
+ await handleRequest({
171
+ request,
172
+ requestId: Reflect.get(request, kRequestId),
173
+ controller,
174
+ emitter: this.emitter,
175
+ })
180
176
  }
181
177
 
182
178
  public onResponse: MockHttpSocketResponseCallback = async ({
@@ -57,7 +57,10 @@ export class XMLHttpRequestController {
57
57
  Array<Function>
58
58
  >
59
59
 
60
- constructor(readonly initialRequest: XMLHttpRequest, public logger: Logger) {
60
+ constructor(
61
+ readonly initialRequest: XMLHttpRequest,
62
+ public logger: Logger
63
+ ) {
61
64
  this[kIsRequestHandled] = false
62
65
 
63
66
  this.events = new Map()
@@ -111,7 +114,7 @@ export class XMLHttpRequestController {
111
114
  case 'addEventListener': {
112
115
  const [eventName, listener] = args as [
113
116
  keyof XMLHttpRequestEventTargetEventMap,
114
- Function
117
+ Function,
115
118
  ]
116
119
 
117
120
  this.registerEvent(eventName, listener)
@@ -131,7 +134,7 @@ export class XMLHttpRequestController {
131
134
 
132
135
  case 'send': {
133
136
  const [body] = args as [
134
- body?: XMLHttpRequestBodyInit | Document | null
137
+ body?: XMLHttpRequestBodyInit | Document | null,
135
138
  ]
136
139
 
137
140
  this.request.addEventListener('load', () => {
@@ -166,38 +169,44 @@ export class XMLHttpRequestController {
166
169
  const fetchRequest = this.toFetchApiRequest(requestBody)
167
170
  this[kFetchRequest] = fetchRequest.clone()
168
171
 
169
- const onceRequestSettled =
170
- this.onRequest?.call(this, {
171
- request: fetchRequest,
172
- requestId: this.requestId!,
173
- }) || Promise.resolve()
174
-
175
- onceRequestSettled.finally(() => {
176
- // If the consumer didn't handle the request (called `.respondWith()`) perform it as-is.
177
- if (!this[kIsRequestHandled]) {
178
- this.logger.info(
179
- 'request callback settled but request has not been handled (readystate %d), performing as-is...',
180
- this.request.readyState
181
- )
182
-
183
- /**
184
- * @note Set the intercepted request ID on the original request in Node.js
185
- * so that if it triggers any other interceptors, they don't attempt
186
- * to process it once again.
187
- *
188
- * For instance, XMLHttpRequest is often implemented via "http.ClientRequest"
189
- * and we don't want for both XHR and ClientRequest interceptors to
190
- * handle the same request at the same time (e.g. emit the "response" event twice).
191
- */
192
- if (IS_NODE) {
193
- this.request.setRequestHeader(
194
- INTERNAL_REQUEST_ID_HEADER_NAME,
195
- this.requestId!
172
+ /**
173
+ * @note Start request handling on the next tick so that the user
174
+ * could add event listeners for "loadend" before the interceptor fires it.
175
+ */
176
+ queueMicrotask(() => {
177
+ const onceRequestSettled =
178
+ this.onRequest?.call(this, {
179
+ request: fetchRequest,
180
+ requestId: this.requestId!,
181
+ }) || Promise.resolve()
182
+
183
+ onceRequestSettled.finally(() => {
184
+ // If the consumer didn't handle the request (called `.respondWith()`) perform it as-is.
185
+ if (!this[kIsRequestHandled]) {
186
+ this.logger.info(
187
+ 'request callback settled but request has not been handled (readystate %d), performing as-is...',
188
+ this.request.readyState
196
189
  )
197
- }
198
190
 
199
- return invoke()
200
- }
191
+ /**
192
+ * @note Set the intercepted request ID on the original request in Node.js
193
+ * so that if it triggers any other interceptors, they don't attempt
194
+ * to process it once again.
195
+ *
196
+ * For instance, XMLHttpRequest is often implemented via "http.ClientRequest"
197
+ * and we don't want for both XHR and ClientRequest interceptors to
198
+ * handle the same request at the same time (e.g. emit the "response" event twice).
199
+ */
200
+ if (IS_NODE) {
201
+ this.request.setRequestHeader(
202
+ INTERNAL_REQUEST_ID_HEADER_NAME,
203
+ this.requestId!
204
+ )
205
+ }
206
+
207
+ return invoke()
208
+ }
209
+ })
201
210
  })
202
211
 
203
212
  break
@@ -241,7 +250,7 @@ export class XMLHttpRequestController {
241
250
  case 'addEventListener': {
242
251
  const [eventName, listener] = args as [
243
252
  keyof XMLHttpRequestEventTargetEventMap,
244
- Function
253
+ Function,
245
254
  ]
246
255
  this.registerUploadEvent(eventName, listener)
247
256
  this.logger.info('upload.addEventListener', eventName, listener)
@@ -312,6 +321,7 @@ export class XMLHttpRequestController {
312
321
  loaded: totalRequestBodyLength,
313
322
  total: totalRequestBodyLength,
314
323
  })
324
+
315
325
  this.trigger('loadend', this.request.upload, {
316
326
  loaded: totalRequestBodyLength,
317
327
  total: totalRequestBodyLength,
@@ -614,7 +624,7 @@ export class XMLHttpRequestController {
614
624
  private trigger<
615
625
  EventName extends keyof (XMLHttpRequestEventTargetEventMap & {
616
626
  readystatechange: ProgressEvent<XMLHttpRequestEventTarget>
617
- })
627
+ }),
618
628
  >(
619
629
  eventName: EventName,
620
630
  target: XMLHttpRequest | XMLHttpRequestUpload,
@@ -3,6 +3,7 @@ import { XMLHttpRequestEmitter } from '.'
3
3
  import { RequestController } from '../../RequestController'
4
4
  import { XMLHttpRequestController } from './XMLHttpRequestController'
5
5
  import { handleRequest } from '../../utils/handleRequest'
6
+ import { isResponseError } from '../../utils/responseUtils'
6
7
 
7
8
  export interface XMLHttpRequestProxyOptions {
8
9
  emitter: XMLHttpRequestEmitter
@@ -52,7 +53,28 @@ export function createXMLHttpRequestProxy({
52
53
  )
53
54
 
54
55
  xhrRequestController.onRequest = async function ({ request, requestId }) {
55
- const controller = new RequestController(request)
56
+ const controller = new RequestController(request, {
57
+ passthrough: () => {
58
+ this.logger.info(
59
+ 'no mocked response received, performing request as-is...'
60
+ )
61
+ },
62
+ respondWith: async (response) => {
63
+ if (isResponseError(response)) {
64
+ this.errorWith(new TypeError('Network error'))
65
+ return
66
+ }
67
+
68
+ await this.respondWith(response)
69
+ },
70
+ errorWith: (reason) => {
71
+ this.logger.info('request errored!', { error: reason })
72
+
73
+ if (reason instanceof Error) {
74
+ this.errorWith(reason)
75
+ }
76
+ },
77
+ })
56
78
 
57
79
  this.logger.info('awaiting mocked response...')
58
80
 
@@ -61,31 +83,12 @@ export function createXMLHttpRequestProxy({
61
83
  emitter.listenerCount('request')
62
84
  )
63
85
 
64
- const isRequestHandled = await handleRequest({
86
+ await handleRequest({
65
87
  request,
66
88
  requestId,
67
89
  controller,
68
90
  emitter,
69
- onResponse: async (response) => {
70
- await 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)
80
- }
81
- },
82
91
  })
83
-
84
- if (!isRequestHandled) {
85
- this.logger.info(
86
- 'no mocked response received, performing request as-is...'
87
- )
88
- }
89
92
  }
90
93
 
91
94
  xhrRequestController.onResponse = async function ({
@@ -1,4 +1,5 @@
1
1
  import { invariant } from 'outvariant'
2
+ import { until } from '@open-draft/until'
2
3
  import { DeferredPromise } from '@open-draft/deferred-promise'
3
4
  import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
4
5
  import { Interceptor } from '../../Interceptor'
@@ -13,6 +14,7 @@ import { decompressResponse } from './utils/decompression'
13
14
  import { hasConfigurableGlobal } from '../../utils/hasConfigurableGlobal'
14
15
  import { FetchResponse } from '../../utils/fetchUtils'
15
16
  import { setRawRequest } from '../../getRawRequest'
17
+ import { isResponseError } from '../../utils/responseUtils'
16
18
 
17
19
  export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
18
20
  static symbol = Symbol('fetch')
@@ -59,22 +61,54 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
59
61
  }
60
62
 
61
63
  const responsePromise = new DeferredPromise<Response>()
62
- const controller = new RequestController(request)
63
64
 
64
- this.logger.info('[%s] %s', request.method, request.url)
65
- this.logger.info('awaiting for the mocked response...')
65
+ const controller = new RequestController(request, {
66
+ passthrough: async () => {
67
+ this.logger.info('request has not been handled, passthrough...')
66
68
 
67
- this.logger.info(
68
- 'emitting the "request" event for %s listener(s)...',
69
- this.emitter.listenerCount('request')
70
- )
69
+ /**
70
+ * @note Clone the request instance right before performing it.
71
+ * This preserves any modifications made to the intercepted request
72
+ * in the "request" listener. This also allows the user to read the
73
+ * request body in the "response" listener (otherwise "unusable").
74
+ */
75
+ const requestCloneForResponseEvent = request.clone()
76
+
77
+ // Perform the intercepted request as-is.
78
+ const { error: responseError, data: originalResponse } = await until(
79
+ () => pureFetch(request)
80
+ )
81
+
82
+ if (responseError) {
83
+ return responsePromise.reject(responseError)
84
+ }
85
+
86
+ this.logger.info('original fetch performed', originalResponse)
87
+
88
+ if (this.emitter.listenerCount('response') > 0) {
89
+ this.logger.info('emitting the "response" event...')
90
+
91
+ const responseClone = originalResponse.clone()
92
+ await emitAsync(this.emitter, 'response', {
93
+ response: responseClone,
94
+ isMockedResponse: false,
95
+ request: requestCloneForResponseEvent,
96
+ requestId,
97
+ })
98
+ }
99
+
100
+ // Resolve the response promise with the original response
101
+ // since the `fetch()` return this internal promise.
102
+ responsePromise.resolve(originalResponse)
103
+ },
104
+ respondWith: async (rawResponse) => {
105
+ // Handle mocked `Response.error()` (i.e. request errors).
106
+ if (isResponseError(rawResponse)) {
107
+ this.logger.info('request has errored!', { response: rawResponse })
108
+ responsePromise.reject(createNetworkError(rawResponse))
109
+ return
110
+ }
71
111
 
72
- const isRequestHandled = await handleRequest({
73
- request,
74
- requestId,
75
- emitter: this.emitter,
76
- controller,
77
- onResponse: async (rawResponse) => {
78
112
  this.logger.info('received mocked response!', {
79
113
  rawResponse,
80
114
  })
@@ -134,51 +168,28 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
134
168
 
135
169
  responsePromise.resolve(response)
136
170
  },
137
- onRequestError: (response) => {
138
- this.logger.info('request has errored!', { response })
139
- responsePromise.reject(createNetworkError(response))
140
- },
141
- onError: (error) => {
142
- this.logger.info('request has been aborted!', { error })
143
- responsePromise.reject(error)
171
+ errorWith: (reason) => {
172
+ this.logger.info('request has been aborted!', { reason })
173
+ responsePromise.reject(reason)
144
174
  },
145
175
  })
146
176
 
147
- if (isRequestHandled) {
148
- this.logger.info('request has been handled, returning mock promise...')
149
- return responsePromise
150
- }
177
+ this.logger.info('[%s] %s', request.method, request.url)
178
+ this.logger.info('awaiting for the mocked response...')
151
179
 
152
180
  this.logger.info(
153
- 'no mocked response received, performing request as-is...'
181
+ 'emitting the "request" event for %s listener(s)...',
182
+ this.emitter.listenerCount('request')
154
183
  )
155
184
 
156
- /**
157
- * @note Clone the request instance right before performing it.
158
- * This preserves any modifications made to the intercepted request
159
- * in the "request" listener. This also allows the user to read the
160
- * request body in the "response" listener (otherwise "unusable").
161
- */
162
- const requestCloneForResponseEvent = request.clone()
163
-
164
- return pureFetch(request).then(async (response) => {
165
- this.logger.info('original fetch performed', response)
166
-
167
- if (this.emitter.listenerCount('response') > 0) {
168
- this.logger.info('emitting the "response" event...')
169
-
170
- const responseClone = response.clone()
171
-
172
- await emitAsync(this.emitter, 'response', {
173
- response: responseClone,
174
- isMockedResponse: false,
175
- request: requestCloneForResponseEvent,
176
- requestId,
177
- })
178
- }
179
-
180
- return response
185
+ await handleRequest({
186
+ request,
187
+ requestId,
188
+ emitter: this.emitter,
189
+ controller,
181
190
  })
191
+
192
+ return responsePromise
182
193
  }
183
194
 
184
195
  Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {
@@ -3,12 +3,11 @@ import { DeferredPromise } from '@open-draft/deferred-promise'
3
3
  import { until } from '@open-draft/until'
4
4
  import type { HttpRequestEventMap } from '../glossary'
5
5
  import { emitAsync } from './emitAsync'
6
- import { kResponsePromise, RequestController } from '../RequestController'
6
+ import { RequestController } from '../RequestController'
7
7
  import {
8
8
  createServerErrorResponse,
9
9
  isResponseError,
10
10
  isResponseLike,
11
- ResponseError,
12
11
  } from './responseUtils'
13
12
  import { InterceptorError } from '../InterceptorError'
14
13
  import { isNodeLikeError } from './isNodeLikeError'
@@ -19,43 +18,22 @@ interface HandleRequestOptions {
19
18
  request: Request
20
19
  emitter: Emitter<HttpRequestEventMap>
21
20
  controller: RequestController
22
-
23
- /**
24
- * Called when the request has been handled
25
- * with the given `Response` instance.
26
- */
27
- onResponse: (response: Response) => void | Promise<void>
28
-
29
- /**
30
- * Called when the request has been handled
31
- * with the given `Response.error()` instance.
32
- */
33
- onRequestError: (response: ResponseError) => void
34
-
35
- /**
36
- * Called when an unhandled error happens during the
37
- * request handling. This is never a thrown error/response.
38
- */
39
- onError: (error: unknown) => void
40
21
  }
41
22
 
42
- /**
43
- * @returns {Promise<boolean>} Indicates whether the request has been handled.
44
- */
45
23
  export async function handleRequest(
46
24
  options: HandleRequestOptions
47
- ): Promise<boolean> {
25
+ ): Promise<void> {
48
26
  const handleResponse = async (
49
27
  response: Response | Error | Record<string, any>
50
28
  ) => {
51
29
  if (response instanceof Error) {
52
- options.onError(response)
30
+ await options.controller.errorWith(response)
53
31
  return true
54
32
  }
55
33
 
56
34
  // Handle "Response.error()" instances.
57
35
  if (isResponseError(response)) {
58
- options.onRequestError(response)
36
+ await options.controller.respondWith(response)
59
37
  return true
60
38
  }
61
39
 
@@ -65,13 +43,13 @@ export async function handleRequest(
65
43
  * since Response instances are, in fact, objects.
66
44
  */
67
45
  if (isResponseLike(response)) {
68
- await options.onResponse(response)
46
+ await options.controller.respondWith(response)
69
47
  return true
70
48
  }
71
49
 
72
50
  // Handle arbitrary objects provided to `.errorWith(reason)`.
73
51
  if (isObject(response)) {
74
- options.onError(response)
52
+ await options.controller.errorWith(response)
75
53
  return true
76
54
  }
77
55
 
@@ -87,7 +65,7 @@ export async function handleRequest(
87
65
 
88
66
  // Support mocking Node.js-like errors.
89
67
  if (isNodeLikeError(error)) {
90
- options.onError(error)
68
+ await options.controller.errorWith(error)
91
69
  return true
92
70
  }
93
71
 
@@ -102,15 +80,14 @@ export async function handleRequest(
102
80
  // Add the last "request" listener to check if the request
103
81
  // has been handled in any way. If it hasn't, resolve the
104
82
  // response promise with undefined.
105
- options.emitter.once('request', ({ requestId: pendingRequestId }) => {
106
- if (pendingRequestId !== options.requestId) {
107
- return
108
- }
109
-
110
- if (options.controller[kResponsePromise].state === 'pending') {
111
- options.controller[kResponsePromise].resolve(undefined)
112
- }
113
- })
83
+ // options.emitter.once('request', async ({ requestId: pendingRequestId }) => {
84
+ // if (
85
+ // pendingRequestId === options.requestId &&
86
+ // options.controller.readyState === RequestController.PENDING
87
+ // ) {
88
+ // await options.controller.passthrough()
89
+ // }
90
+ // })
114
91
 
115
92
  const requestAbortPromise = new DeferredPromise<void, unknown>()
116
93
 
@@ -119,16 +96,17 @@ export async function handleRequest(
119
96
  */
120
97
  if (options.request.signal) {
121
98
  if (options.request.signal.aborted) {
122
- requestAbortPromise.reject(options.request.signal.reason)
123
- } else {
124
- options.request.signal.addEventListener(
125
- 'abort',
126
- () => {
127
- requestAbortPromise.reject(options.request.signal.reason)
128
- },
129
- { once: true }
130
- )
99
+ await options.controller.errorWith(options.request.signal.reason)
100
+ return
131
101
  }
102
+
103
+ options.request.signal.addEventListener(
104
+ 'abort',
105
+ () => {
106
+ requestAbortPromise.reject(options.request.signal.reason)
107
+ },
108
+ { once: true }
109
+ )
132
110
  }
133
111
 
134
112
  const result = await until(async () => {
@@ -146,25 +124,21 @@ export async function handleRequest(
146
124
  // Short-circuit the request handling promise if the request gets aborted.
147
125
  requestAbortPromise,
148
126
  requestListenersPromise,
149
- options.controller[kResponsePromise],
127
+ options.controller.handled,
150
128
  ])
151
-
152
- // The response promise will settle immediately once
153
- // the developer calls either "respondWith" or "errorWith".
154
- return await options.controller[kResponsePromise]
155
129
  })
156
130
 
157
131
  // Handle the request being aborted while waiting for the request listeners.
158
132
  if (requestAbortPromise.state === 'rejected') {
159
- options.onError(requestAbortPromise.rejectionReason)
160
- return true
133
+ await options.controller.errorWith(requestAbortPromise.rejectionReason)
134
+ return
161
135
  }
162
136
 
163
137
  if (result.error) {
164
138
  // Handle the error during the request listener execution.
165
139
  // These can be thrown responses or request errors.
166
140
  if (await handleResponseError(result.error)) {
167
- return true
141
+ return
168
142
  }
169
143
 
170
144
  // If the developer has added "unhandledException" listeners,
@@ -175,7 +149,28 @@ export async function handleRequest(
175
149
  // This is needed because the original controller might have been already
176
150
  // interacted with (e.g. "respondWith" or "errorWith" called on it).
177
151
  const unhandledExceptionController = new RequestController(
178
- options.request
152
+ options.request,
153
+ {
154
+ /**
155
+ * @note Intentionally empty passthrough handle.
156
+ * This controller is created within another controller and we only need
157
+ * to know if `unhandledException` listeners handled the request.
158
+ */
159
+ passthrough() {},
160
+ async respondWith(response) {
161
+ await handleResponse(response)
162
+ },
163
+ async errorWith(reason) {
164
+ /**
165
+ * @note Handle the result of the unhandled controller
166
+ * in the same way as the original request controller.
167
+ * The exception here is that thrown errors within the
168
+ * "unhandledException" event do NOT result in another
169
+ * emit of the same event. They are forwarded as-is.
170
+ */
171
+ await options.controller.errorWith(reason)
172
+ },
173
+ }
179
174
  )
180
175
 
181
176
  await emitAsync(options.emitter, 'unhandledException', {
@@ -183,53 +178,28 @@ export async function handleRequest(
183
178
  request: options.request,
184
179
  requestId: options.requestId,
185
180
  controller: unhandledExceptionController,
186
- }).then(() => {
187
- // If all the "unhandledException" listeners have finished
188
- // but have not handled the response in any way, preemptively
189
- // resolve the pending response promise from the new controller.
190
- // This prevents it from hanging forever.
191
- if (
192
- unhandledExceptionController[kResponsePromise].state === 'pending'
193
- ) {
194
- unhandledExceptionController[kResponsePromise].resolve(undefined)
195
- }
196
181
  })
197
182
 
198
- const nextResult = await until(
199
- () => unhandledExceptionController[kResponsePromise]
200
- )
201
-
202
- /**
203
- * @note Handle the result of the unhandled controller
204
- * in the same way as the original request controller.
205
- * The exception here is that thrown errors within the
206
- * "unhandledException" event do NOT result in another
207
- * emit of the same event. They are forwarded as-is.
208
- */
209
- if (nextResult.error) {
210
- return handleResponseError(nextResult.error)
211
- }
212
-
213
- if (nextResult.data) {
214
- return handleResponse(nextResult.data)
183
+ // If all the "unhandledException" listeners have finished
184
+ // but have not handled the request in any way, passthrough.
185
+ if (
186
+ unhandledExceptionController.readyState !== RequestController.PENDING
187
+ ) {
188
+ return
215
189
  }
216
190
  }
217
191
 
218
192
  // Otherwise, coerce unhandled exceptions to a 500 Internal Server Error response.
219
- options.onResponse(createServerErrorResponse(result.error))
220
- return true
193
+ await options.controller.respondWith(
194
+ createServerErrorResponse(result.error)
195
+ )
196
+ return
221
197
  }
222
198
 
223
- /**
224
- * Handle a mocked Response instance.
225
- * @note That this can also be an Error in case
226
- * the developer called "errorWith". This differentiates
227
- * unhandled exceptions from intended errors.
228
- */
229
- if (result.data) {
230
- return handleResponse(result.data)
199
+ // If the request hasn't been handled by this point, passthrough.
200
+ if (options.controller.readyState === RequestController.PENDING) {
201
+ return await options.controller.passthrough()
231
202
  }
232
203
 
233
- // In all other cases, consider the request unhandled.
234
- return false
204
+ return options.controller.handled
235
205
  }