@mswjs/interceptors 0.32.2 → 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 (118) 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-CFRXZJO4.js → chunk-2MWIWEWV.js} +31 -72
  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-CMVICWQS.mjs → chunk-CU3YXMM4.mjs} +23 -64
  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 +1 -1
  84. package/src/interceptors/ClientRequest/index.test.ts +2 -33
  85. package/src/interceptors/ClientRequest/index.ts +21 -82
  86. package/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts +1 -1
  87. package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +27 -108
  88. package/src/interceptors/XMLHttpRequest/index.ts +0 -6
  89. package/src/interceptors/fetch/index.ts +52 -169
  90. package/src/utils/handleRequest.ts +213 -0
  91. package/src/utils/responseUtils.ts +4 -4
  92. package/lib/browser/chunk-2CRB3JAQ.js.map +0 -1
  93. package/lib/browser/chunk-732REFPX.mjs.map +0 -1
  94. package/lib/browser/chunk-MAEPOYB6.mjs +0 -213
  95. package/lib/browser/chunk-MAEPOYB6.mjs.map +0 -1
  96. package/lib/browser/chunk-MQJ3JOOK.js +0 -49
  97. package/lib/browser/chunk-MQJ3JOOK.js.map +0 -1
  98. package/lib/browser/chunk-OMISYKWR.mjs.map +0 -1
  99. package/lib/browser/chunk-OUWBQF3Z.mjs +0 -49
  100. package/lib/browser/chunk-OUWBQF3Z.mjs.map +0 -1
  101. package/lib/browser/chunk-PSX5J3RF.js.map +0 -1
  102. package/lib/browser/chunk-WBHIW62P.js +0 -213
  103. package/lib/browser/chunk-WBHIW62P.js.map +0 -1
  104. package/lib/browser/glossary-1c204f45.d.ts +0 -44
  105. package/lib/node/chunk-BFLYGQ6D.js.map +0 -1
  106. package/lib/node/chunk-CFRXZJO4.js.map +0 -1
  107. package/lib/node/chunk-CMVICWQS.mjs.map +0 -1
  108. package/lib/node/chunk-DV4PBH4D.mjs.map +0 -1
  109. package/lib/node/chunk-EIBTX65O.js.map +0 -1
  110. package/lib/node/chunk-KWV3JXSI.mjs +0 -49
  111. package/lib/node/chunk-KWV3JXSI.mjs.map +0 -1
  112. package/lib/node/chunk-OJ6O4LSC.mjs.map +0 -1
  113. package/lib/node/chunk-PYD4E2EJ.js.map +0 -1
  114. package/lib/node/chunk-UXCYRE4F.js +0 -49
  115. package/lib/node/chunk-UXCYRE4F.js.map +0 -1
  116. package/src/utils/toInteractiveRequest.ts +0 -23
  117. /package/lib/node/{chunk-2COJKQQB.js.map → chunk-42632LKH.js.map} +0 -0
  118. /package/lib/node/{chunk-PNWPIDEL.mjs.map → chunk-HGQLG7KE.mjs.map} +0 -0
package/src/glossary.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { InteractiveRequest } from './utils/toInteractiveRequest'
1
+ import type { RequestController } from './RequestController'
2
2
 
3
3
  export const IS_PATCHED_MODULE: unique symbol = Symbol('isPatchedModule')
4
4
 
@@ -7,8 +7,9 @@ export type RequestCredentials = 'omit' | 'include' | 'same-origin'
7
7
  export type HttpRequestEventMap = {
8
8
  request: [
9
9
  args: {
10
- request: InteractiveRequest
10
+ request: Request
11
11
  requestId: string
12
+ controller: RequestController
12
13
  }
13
14
  ]
14
15
  response: [
@@ -24,10 +25,7 @@ export type HttpRequestEventMap = {
24
25
  error: unknown
25
26
  request: Request
26
27
  requestId: string
27
- controller: {
28
- respondWith(response: Response): void
29
- errorWith(error?: Error): void
30
- }
28
+ controller: RequestController
31
29
  }
32
30
  ]
33
31
  }
@@ -378,7 +378,7 @@ export class MockHttpSocket extends MockSocket {
378
378
  /**
379
379
  * Close this socket connection with the given error.
380
380
  */
381
- public errorWith(error: Error): void {
381
+ public errorWith(error?: Error): void {
382
382
  this.destroy(error)
383
383
  }
384
384
 
@@ -30,43 +30,12 @@ afterAll(async () => {
30
30
  await httpServer.close()
31
31
  })
32
32
 
33
- it('forbids calling "respondWith" multiple times for the same request', async () => {
34
- const requestUrl = httpServer.http.url('/')
35
-
36
- interceptor.on('request', function firstRequestListener({ request }) {
37
- request.respondWith(new Response())
38
- })
39
-
40
- const secondRequestEmitted = new DeferredPromise<void>()
41
- interceptor.on('request', function secondRequestListener({ request }) {
42
- expect(() =>
43
- request.respondWith(new Response(null, { status: 301 }))
44
- ).toThrow(
45
- `Failed to respond to "GET ${requestUrl}" request: the "request" event has already been responded to.`
46
- )
47
-
48
- secondRequestEmitted.resolve()
49
- })
50
-
51
- const request = http.get(requestUrl)
52
- await secondRequestEmitted
53
-
54
- const responseReceived = new DeferredPromise<http.IncomingMessage>()
55
- request.on('response', (response) => {
56
- responseReceived.resolve(response)
57
- })
58
-
59
- const response = await responseReceived
60
- expect(response.statusCode).toBe(200)
61
- expect(response.statusMessage).toBe('OK')
62
- })
63
-
64
33
  it('abort the request if the abort signal is emitted', async () => {
65
34
  const requestUrl = httpServer.http.url('/')
66
35
 
67
- interceptor.on('request', async function delayedResponse({ request }) {
36
+ interceptor.on('request', async function delayedResponse({ controller }) {
68
37
  await sleep(1_000)
69
- request.respondWith(new Response())
38
+ controller.respondWith(new Response())
70
39
  })
71
40
 
72
41
  const abortController = new AbortController()
@@ -1,6 +1,5 @@
1
1
  import http from 'node:http'
2
2
  import https from 'node:https'
3
- import { until } from '@open-draft/until'
4
3
  import { Interceptor } from '../../Interceptor'
5
4
  import type { HttpRequestEventMap } from '../../glossary'
6
5
  import {
@@ -9,11 +8,10 @@ import {
9
8
  MockHttpSocketResponseCallback,
10
9
  } from './MockHttpSocket'
11
10
  import { MockAgent, MockHttpsAgent } from './agents'
11
+ import { RequestController } from '../../RequestController'
12
12
  import { emitAsync } from '../../utils/emitAsync'
13
- import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
14
13
  import { normalizeClientRequestArgs } from './utils/normalizeClientRequestArgs'
15
- import { isNodeLikeError } from '../../utils/isNodeLikeError'
16
- import { createServerErrorResponse } from '../../utils/responseUtils'
14
+ import { handleRequest } from '../../utils/handleRequest'
17
15
  import {
18
16
  recordRawFetchHeaders,
19
17
  restoreHeadersPrototype,
@@ -129,88 +127,29 @@ export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
129
127
  socket,
130
128
  }) => {
131
129
  const requestId = Reflect.get(request, kRequestId)
132
- const { interactiveRequest, requestController } =
133
- toInteractiveRequest(request)
134
-
135
- // TODO: Abstract this bit. We are using it everywhere.
136
- this.emitter.once('request', ({ requestId: pendingRequestId }) => {
137
- if (pendingRequestId !== requestId) {
138
- return
139
- }
140
-
141
- if (requestController.responsePromise.state === 'pending') {
142
- this.logger.info(
143
- 'request has not been handled in listeners, executing fail-safe listener...'
144
- )
145
-
146
- requestController.responsePromise.resolve(undefined)
147
- }
148
- })
130
+ const controller = new RequestController(request)
149
131
 
150
- const listenerResult = await until(async () => {
151
- await emitAsync(this.emitter, 'request', {
152
- requestId,
153
- request: interactiveRequest,
154
- })
155
-
156
- return await requestController.responsePromise
157
- })
158
-
159
- if (listenerResult.error) {
160
- // Treat thrown Responses as mocked responses.
161
- if (listenerResult.error instanceof Response) {
162
- socket.respondWith(listenerResult.error)
163
- return
164
- }
165
-
166
- // Allow mocking Node-like errors.
167
- if (isNodeLikeError(listenerResult.error)) {
168
- socket.errorWith(listenerResult.error)
169
- return
170
- }
171
-
172
- // Emit the "unhandledException" event to allow the client
173
- // to opt-out from the default handling of exceptions
174
- // as 500 error responses.
175
- if (this.emitter.listenerCount('unhandledException') > 0) {
176
- await emitAsync(this.emitter, 'unhandledException', {
177
- error: listenerResult.error,
178
- request,
179
- requestId,
180
- controller: {
181
- respondWith: socket.respondWith.bind(socket),
182
- errorWith: socket.errorWith.bind(socket),
183
- },
184
- })
185
-
186
- // After the listeners are done, if the socket is
187
- // not connecting anymore, the response was mocked.
188
- // If the socket has been destroyed, the error was mocked.
189
- // Treat both as the result of the listener's call.
190
- if (!socket.connecting || socket.destroyed) {
191
- return
132
+ const isRequestHandled = await handleRequest({
133
+ request,
134
+ requestId,
135
+ controller,
136
+ emitter: this.emitter,
137
+ onResponse: (response) => {
138
+ socket.respondWith(response)
139
+ },
140
+ onRequestError: (response) => {
141
+ socket.respondWith(response)
142
+ },
143
+ onError: (error) => {
144
+ if (error instanceof Error) {
145
+ socket.errorWith(error)
192
146
  }
193
- }
194
-
195
- // Unhandled exceptions in the request listeners are
196
- // synonymous to unhandled exceptions on the server.
197
- // Those are represented as 500 error responses.
198
- socket.respondWith(createServerErrorResponse(listenerResult.error))
199
- return
200
- }
201
-
202
- const mockedResponse = listenerResult.data
147
+ },
148
+ })
203
149
 
204
- if (mockedResponse) {
205
- /**
206
- * @note The `.respondWith()` method will handle "Response.error()".
207
- * Maybe we should make all interceptors do that?
208
- */
209
- socket.respondWith(mockedResponse)
210
- return
150
+ if (!isRequestHandled) {
151
+ return socket.passthrough()
211
152
  }
212
-
213
- socket.passthrough()
214
153
  }
215
154
 
216
155
  public onResponse: MockHttpSocketResponseCallback = async ({
@@ -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
  }