@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
@@ -1,21 +1,19 @@
1
1
  import {
2
2
  IS_PATCHED_MODULE
3
- } from "../../chunk-OJ6O4LSC.mjs";
3
+ } from "../../chunk-BZ3Y7YV5.mjs";
4
4
  import {
5
+ RequestController,
5
6
  emitAsync,
6
- toInteractiveRequest
7
- } from "../../chunk-KWV3JXSI.mjs";
7
+ handleRequest
8
+ } from "../../chunk-KY3RJ2M3.mjs";
8
9
  import {
9
10
  Interceptor,
10
- createRequestId,
11
- createServerErrorResponse,
12
- isResponseError
13
- } from "../../chunk-TGTPXCLF.mjs";
11
+ createRequestId
12
+ } from "../../chunk-BUCULLYM.mjs";
14
13
 
15
14
  // src/interceptors/fetch/index.ts
16
15
  import { invariant } from "outvariant";
17
16
  import { DeferredPromise } from "@open-draft/deferred-promise";
18
- import { until } from "@open-draft/until";
19
17
 
20
18
  // src/utils/canParseUrl.ts
21
19
  function canParseUrl(url) {
@@ -42,131 +40,62 @@ var _FetchInterceptor = class extends Interceptor {
42
40
  'Failed to patch the "fetch" module: already patched.'
43
41
  );
44
42
  globalThis.fetch = async (input, init) => {
45
- var _a;
46
43
  const requestId = createRequestId();
47
44
  const resolvedInput = typeof input === "string" && typeof location !== "undefined" && !canParseUrl(input) ? new URL(input, location.origin) : input;
48
45
  const request = new Request(resolvedInput, init);
46
+ const responsePromise = new DeferredPromise();
47
+ const controller = new RequestController(request);
49
48
  this.logger.info("[%s] %s", request.method, request.url);
50
- const { interactiveRequest, requestController } = toInteractiveRequest(request);
49
+ this.logger.info("awaiting for the mocked response...");
51
50
  this.logger.info(
52
- 'emitting the "request" event for %d listener(s)...',
51
+ 'emitting the "request" event for %s listener(s)...',
53
52
  this.emitter.listenerCount("request")
54
53
  );
55
- this.emitter.once("request", ({ requestId: pendingRequestId }) => {
56
- if (pendingRequestId !== requestId) {
57
- return;
58
- }
59
- if (requestController.responsePromise.state === "pending") {
60
- requestController.responsePromise.resolve(void 0);
61
- }
62
- });
63
- this.logger.info("awaiting for the mocked response...");
64
- const signal = interactiveRequest.signal;
65
- const requestAborted = new DeferredPromise();
66
- if (signal) {
67
- signal.addEventListener(
68
- "abort",
69
- () => {
70
- requestAborted.reject(signal.reason);
71
- },
72
- { once: true }
73
- );
74
- }
75
- const responsePromise = new DeferredPromise();
76
- const respondWith = (response) => {
77
- this.logger.info("responding with a mock response:", response);
78
- if (this.emitter.listenerCount("response") > 0) {
79
- this.logger.info('emitting the "response" event...');
80
- const responseClone = response.clone();
81
- this.emitter.emit("response", {
82
- response: responseClone,
83
- isMockedResponse: true,
84
- request: interactiveRequest,
85
- requestId
54
+ const isRequestHandled = await handleRequest({
55
+ request,
56
+ requestId,
57
+ emitter: this.emitter,
58
+ controller,
59
+ onResponse: async (response) => {
60
+ this.logger.info("received mocked response!", {
61
+ response
86
62
  });
87
- }
88
- Object.defineProperty(response, "url", {
89
- writable: false,
90
- enumerable: true,
91
- configurable: false,
92
- value: request.url
93
- });
94
- responsePromise.resolve(response);
95
- };
96
- const errorWith = (reason) => {
97
- responsePromise.reject(reason);
98
- };
99
- const resolverResult = await until(
100
- async () => {
101
- const listenersFinished = emitAsync(this.emitter, "request", {
102
- request: interactiveRequest,
103
- requestId
104
- });
105
- await Promise.race([
106
- requestAborted,
107
- // Put the listeners invocation Promise in the same race condition
108
- // with the request abort Promise because otherwise awaiting the listeners
109
- // would always yield some response (or undefined).
110
- listenersFinished,
111
- requestController.responsePromise
112
- ]);
113
- this.logger.info("all request listeners have been resolved!");
114
- const mockedResponse2 = await requestController.responsePromise;
115
- this.logger.info("event.respondWith called with:", mockedResponse2);
116
- return mockedResponse2;
117
- }
118
- );
119
- if (requestAborted.state === "rejected") {
120
- this.logger.info(
121
- "request has been aborted:",
122
- requestAborted.rejectionReason
123
- );
124
- responsePromise.reject(requestAborted.rejectionReason);
125
- return responsePromise;
126
- }
127
- if (resolverResult.error) {
128
- this.logger.info(
129
- "request listerner threw an error:",
130
- resolverResult.error
131
- );
132
- if (resolverResult.error instanceof Response) {
133
- if (isResponseError(resolverResult.error)) {
134
- errorWith(createNetworkError(resolverResult.error));
135
- } else {
136
- respondWith(resolverResult.error);
63
+ if (this.emitter.listenerCount("response") > 0) {
64
+ this.logger.info('emitting the "response" event...');
65
+ await emitAsync(this.emitter, "response", {
66
+ // Clone the mocked response for the "response" event listener.
67
+ // This way, the listener can read the response and not lock its body
68
+ // for the actual fetch consumer.
69
+ response: response.clone(),
70
+ isMockedResponse: true,
71
+ request,
72
+ requestId
73
+ });
137
74
  }
138
- }
139
- if (this.emitter.listenerCount("unhandledException") > 0) {
140
- await emitAsync(this.emitter, "unhandledException", {
141
- error: resolverResult.error,
142
- request,
143
- requestId,
144
- controller: {
145
- respondWith,
146
- errorWith
147
- }
75
+ Object.defineProperty(response, "url", {
76
+ writable: false,
77
+ enumerable: true,
78
+ configurable: false,
79
+ value: request.url
148
80
  });
149
- if (responsePromise.state !== "pending") {
150
- return responsePromise;
151
- }
152
- }
153
- respondWith(createServerErrorResponse(resolverResult.error));
154
- return responsePromise;
155
- }
156
- const mockedResponse = resolverResult.data;
157
- if (mockedResponse && !((_a = request.signal) == null ? void 0 : _a.aborted)) {
158
- this.logger.info("received mocked response:", mockedResponse);
159
- if (isResponseError(mockedResponse)) {
160
- this.logger.info(
161
- "received a network error response, rejecting the request promise..."
162
- );
163
- errorWith(createNetworkError(mockedResponse));
164
- } else {
165
- respondWith(mockedResponse);
81
+ responsePromise.resolve(response);
82
+ },
83
+ onRequestError: (response) => {
84
+ this.logger.info("request has errored!", { response });
85
+ responsePromise.reject(createNetworkError(response));
86
+ },
87
+ onError: (error) => {
88
+ this.logger.info("request has been aborted!", { error });
89
+ responsePromise.reject(error);
166
90
  }
91
+ });
92
+ if (isRequestHandled) {
93
+ this.logger.info("request has been handled, returning mock promise...");
167
94
  return responsePromise;
168
95
  }
169
- this.logger.info("no mocked response received!");
96
+ this.logger.info(
97
+ "no mocked response received, performing request as-is..."
98
+ );
170
99
  return pureFetch(request).then((response) => {
171
100
  this.logger.info("original fetch performed", response);
172
101
  if (this.emitter.listenerCount("response") > 0) {
@@ -175,7 +104,7 @@ var _FetchInterceptor = class extends Interceptor {
175
104
  this.emitter.emit("response", {
176
105
  response: responseClone,
177
106
  isMockedResponse: false,
178
- request: interactiveRequest,
107
+ request,
179
108
  requestId
180
109
  });
181
110
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/interceptors/fetch/index.ts","../../../../src/utils/canParseUrl.ts"],"sourcesContent":["import { invariant } from 'outvariant'\nimport { DeferredPromise } from '@open-draft/deferred-promise'\nimport { until } from '@open-draft/until'\nimport { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'\nimport { Interceptor } from '../../Interceptor'\nimport { toInteractiveRequest } from '../../utils/toInteractiveRequest'\nimport { emitAsync } from '../../utils/emitAsync'\nimport { canParseUrl } from '../../utils/canParseUrl'\nimport { createRequestId } from '../../createRequestId'\nimport {\n createServerErrorResponse,\n isResponseError,\n} from '../../utils/responseUtils'\n\nexport class FetchInterceptor extends Interceptor<HttpRequestEventMap> {\n static symbol = Symbol('fetch')\n\n constructor() {\n super(FetchInterceptor.symbol)\n }\n\n protected checkEnvironment() {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.fetch !== 'undefined'\n )\n }\n\n protected async setup() {\n const pureFetch = globalThis.fetch\n\n invariant(\n !(pureFetch as any)[IS_PATCHED_MODULE],\n 'Failed to patch the \"fetch\" module: already patched.'\n )\n\n globalThis.fetch = 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 !canParseUrl(input)\n ? new URL(input, location.origin)\n : input\n\n const request = new Request(resolvedInput, init)\n\n this.logger.info('[%s] %s', request.method, request.url)\n\n const { interactiveRequest, requestController } =\n toInteractiveRequest(request)\n\n this.logger.info(\n 'emitting the \"request\" event for %d listener(s)...',\n this.emitter.listenerCount('request')\n )\n\n this.emitter.once('request', ({ requestId: pendingRequestId }) => {\n if (pendingRequestId !== requestId) {\n return\n }\n\n if (requestController.responsePromise.state === 'pending') {\n requestController.responsePromise.resolve(undefined)\n }\n })\n\n this.logger.info('awaiting for the mocked response...')\n\n const signal = interactiveRequest.signal\n const requestAborted = new DeferredPromise()\n\n // Signal isn't always defined in react-native.\n if (signal) {\n signal.addEventListener(\n 'abort',\n () => {\n requestAborted.reject(signal.reason)\n },\n { once: true }\n )\n }\n\n const responsePromise = new DeferredPromise<Response>()\n\n const respondWith = (response: Response): void => {\n this.logger.info('responding with a mock response:', response)\n\n if (this.emitter.listenerCount('response') > 0) {\n this.logger.info('emitting the \"response\" event...')\n\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 const responseClone = response.clone()\n\n this.emitter.emit('response', {\n response: responseClone,\n isMockedResponse: true,\n request: interactiveRequest,\n requestId,\n })\n }\n\n // Set the \"response.url\" property to equal the intercepted request URL.\n Object.defineProperty(response, 'url', {\n writable: false,\n enumerable: true,\n configurable: false,\n value: request.url,\n })\n\n responsePromise.resolve(response)\n }\n\n const errorWith = (reason: unknown): void => {\n responsePromise.reject(reason)\n }\n\n const resolverResult = await until<unknown, Response | undefined>(\n async () => {\n const listenersFinished = emitAsync(this.emitter, 'request', {\n request: interactiveRequest,\n requestId,\n })\n\n await Promise.race([\n requestAborted,\n // Put the listeners invocation Promise in the same race condition\n // with the request abort Promise because otherwise awaiting the listeners\n // would always yield some response (or undefined).\n listenersFinished,\n requestController.responsePromise,\n ])\n\n this.logger.info('all request listeners have been resolved!')\n\n const mockedResponse = await requestController.responsePromise\n this.logger.info('event.respondWith called with:', mockedResponse)\n\n return mockedResponse\n }\n )\n\n if (requestAborted.state === 'rejected') {\n this.logger.info(\n 'request has been aborted:',\n requestAborted.rejectionReason\n )\n\n responsePromise.reject(requestAborted.rejectionReason)\n return responsePromise\n }\n\n if (resolverResult.error) {\n this.logger.info(\n 'request listerner threw an error:',\n resolverResult.error\n )\n\n // Treat thrown Responses as mocked responses.\n if (resolverResult.error instanceof Response) {\n // Treat thrown Response.error() as a request error.\n if (isResponseError(resolverResult.error)) {\n errorWith(createNetworkError(resolverResult.error))\n } else {\n // Treat the rest of thrown Responses as mocked responses.\n respondWith(resolverResult.error)\n }\n }\n\n // Emit the \"unhandledException\" interceptor event so the client\n // can opt-out from exceptions translating to 500 error responses.\n\n if (this.emitter.listenerCount('unhandledException') > 0) {\n await emitAsync(this.emitter, 'unhandledException', {\n error: resolverResult.error,\n request,\n requestId,\n controller: {\n respondWith,\n errorWith,\n },\n })\n\n if (responsePromise.state !== 'pending') {\n return responsePromise\n }\n }\n\n // Unhandled exceptions in the request listeners are\n // synonymous to unhandled exceptions on the server.\n // Those are represented as 500 error responses.\n respondWith(createServerErrorResponse(resolverResult.error))\n return responsePromise\n }\n\n const mockedResponse = resolverResult.data\n\n if (mockedResponse && !request.signal?.aborted) {\n this.logger.info('received mocked response:', mockedResponse)\n\n // Reject the request Promise on mocked \"Response.error\" responses.\n if (isResponseError(mockedResponse)) {\n this.logger.info(\n 'received a network error response, rejecting the request promise...'\n )\n\n /**\n * Set the cause of the request promise rejection to the\n * network error Response instance. This differs from Undici.\n * Undici will forward the \"response.error\" custom property\n * as the rejection reason but for \"Response.error()\" static method\n * \"response.error\" will equal to undefined, making \"cause\" an empty Error.\n * @see https://github.com/nodejs/undici/blob/83cb522ae0157a19d149d72c7d03d46e34510d0a/lib/fetch/response.js#L344\n */\n errorWith(createNetworkError(mockedResponse))\n } else {\n respondWith(mockedResponse)\n }\n\n return responsePromise\n }\n\n this.logger.info('no mocked response received!')\n\n return pureFetch(request).then((response) => {\n this.logger.info('original fetch performed', response)\n\n if (this.emitter.listenerCount('response') > 0) {\n this.logger.info('emitting the \"response\" event...')\n\n const responseClone = response.clone()\n\n this.emitter.emit('response', {\n response: responseClone,\n isMockedResponse: false,\n request: interactiveRequest,\n requestId,\n })\n }\n\n return response\n })\n }\n\n Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {\n enumerable: true,\n configurable: true,\n value: true,\n })\n\n this.subscriptions.push(() => {\n Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {\n value: undefined,\n })\n\n globalThis.fetch = pureFetch\n\n this.logger.info(\n 'restored native \"globalThis.fetch\"!',\n globalThis.fetch.name\n )\n })\n }\n}\n\nfunction createNetworkError(cause: unknown) {\n return Object.assign(new TypeError('Failed to fetch'), {\n cause,\n })\n}\n","/**\n * Returns a boolean indicating whether the given URL string\n * can be parsed into a `URL` instance.\n * A substitute for `URL.canParse()` for Node.js 18.\n */\nexport function canParseUrl(url: string): boolean {\n try {\n new URL(url)\n return true\n } catch (_error) {\n return false\n }\n}\n"],"mappings":"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","names":["mockedResponse"]}
1
+ {"version":3,"sources":["../../../../src/interceptors/fetch/index.ts","../../../../src/utils/canParseUrl.ts"],"sourcesContent":["import { invariant } from 'outvariant'\nimport { DeferredPromise } from '@open-draft/deferred-promise'\nimport { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'\nimport { Interceptor } from '../../Interceptor'\nimport { RequestController } from '../../RequestController'\nimport { emitAsync } from '../../utils/emitAsync'\nimport { handleRequest } from '../../utils/handleRequest'\nimport { canParseUrl } from '../../utils/canParseUrl'\nimport { createRequestId } from '../../createRequestId'\n\nexport class FetchInterceptor extends Interceptor<HttpRequestEventMap> {\n static symbol = Symbol('fetch')\n\n constructor() {\n super(FetchInterceptor.symbol)\n }\n\n protected checkEnvironment() {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.fetch !== 'undefined'\n )\n }\n\n protected async setup() {\n const pureFetch = globalThis.fetch\n\n invariant(\n !(pureFetch as any)[IS_PATCHED_MODULE],\n 'Failed to patch the \"fetch\" module: already patched.'\n )\n\n globalThis.fetch = 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 !canParseUrl(input)\n ? new URL(input, location.origin)\n : input\n\n const request = new Request(resolvedInput, init)\n const responsePromise = new DeferredPromise<Response>()\n const controller = new RequestController(request)\n\n this.logger.info('[%s] %s', request.method, request.url)\n this.logger.info('awaiting for the mocked response...')\n\n this.logger.info(\n 'emitting the \"request\" event for %s listener(s)...',\n this.emitter.listenerCount('request')\n )\n\n const isRequestHandled = await handleRequest({\n request,\n requestId,\n emitter: this.emitter,\n controller,\n onResponse: async (response) => {\n this.logger.info('received mocked response!', {\n response,\n })\n\n if (this.emitter.listenerCount('response') > 0) {\n this.logger.info('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 emitAsync(this.emitter, 'response', {\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: response.clone(),\n isMockedResponse: true,\n request,\n requestId,\n })\n }\n\n // Set the \"response.url\" property to equal the intercepted request URL.\n Object.defineProperty(response, 'url', {\n writable: false,\n enumerable: true,\n configurable: false,\n value: request.url,\n })\n\n responsePromise.resolve(response)\n },\n onRequestError: (response) => {\n this.logger.info('request has errored!', { response })\n responsePromise.reject(createNetworkError(response))\n },\n onError: (error) => {\n this.logger.info('request has been aborted!', { error })\n responsePromise.reject(error)\n },\n })\n\n if (isRequestHandled) {\n this.logger.info('request has been handled, returning mock promise...')\n return responsePromise\n }\n\n this.logger.info(\n 'no mocked response received, performing request as-is...'\n )\n\n return pureFetch(request).then((response) => {\n this.logger.info('original fetch performed', response)\n\n if (this.emitter.listenerCount('response') > 0) {\n this.logger.info('emitting the \"response\" event...')\n\n const responseClone = response.clone()\n\n this.emitter.emit('response', {\n response: responseClone,\n isMockedResponse: false,\n request,\n requestId,\n })\n }\n\n return response\n })\n }\n\n Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {\n enumerable: true,\n configurable: true,\n value: true,\n })\n\n this.subscriptions.push(() => {\n Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {\n value: undefined,\n })\n\n globalThis.fetch = pureFetch\n\n this.logger.info(\n 'restored native \"globalThis.fetch\"!',\n globalThis.fetch.name\n )\n })\n }\n}\n\nfunction createNetworkError(cause: unknown) {\n return Object.assign(new TypeError('Failed to fetch'), {\n cause,\n })\n}\n","/**\n * Returns a boolean indicating whether the given URL string\n * can be parsed into a `URL` instance.\n * A substitute for `URL.canParse()` for Node.js 18.\n */\nexport function canParseUrl(url: string): boolean {\n try {\n new URL(url)\n return true\n } catch (_error) {\n return false\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import { ClientRequestInterceptor } from '../interceptors/ClientRequest/index.js';
2
2
  import { XMLHttpRequestInterceptor } from '../interceptors/XMLHttpRequest/index.js';
3
- import '../Interceptor-88ee47c0.js';
3
+ import '../Interceptor-a31b1217.js';
4
4
  import '@open-draft/deferred-promise';
5
5
  import '@open-draft/logger';
6
6
  import 'strict-event-emitter';
@@ -1,18 +1,18 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunkCFRXZJO4js = require('../chunk-CFRXZJO4.js');
3
+ var _chunk2MWIWEWVjs = require('../chunk-2MWIWEWV.js');
4
4
 
5
5
 
6
- var _chunkPYD4E2EJjs = require('../chunk-PYD4E2EJ.js');
6
+ var _chunkP6QG76R3js = require('../chunk-P6QG76R3.js');
7
7
  require('../chunk-LK6DILFK.js');
8
- require('../chunk-EIBTX65O.js');
9
- require('../chunk-UXCYRE4F.js');
10
- require('../chunk-BFLYGQ6D.js');
8
+ require('../chunk-IDEEMJ3F.js');
9
+ require('../chunk-5WWNCLB3.js');
10
+ require('../chunk-YGM3BCJU.js');
11
11
 
12
12
  // src/presets/node.ts
13
13
  var node_default = [
14
- new (0, _chunkCFRXZJO4js.ClientRequestInterceptor)(),
15
- new (0, _chunkPYD4E2EJjs.XMLHttpRequestInterceptor)()
14
+ new (0, _chunk2MWIWEWVjs.ClientRequestInterceptor)(),
15
+ new (0, _chunkP6QG76R3js.XMLHttpRequestInterceptor)()
16
16
  ];
17
17
 
18
18
 
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  ClientRequestInterceptor
3
- } from "../chunk-CMVICWQS.mjs";
3
+ } from "../chunk-CU3YXMM4.mjs";
4
4
  import {
5
5
  XMLHttpRequestInterceptor
6
- } from "../chunk-DV4PBH4D.mjs";
6
+ } from "../chunk-TOV4TYIX.mjs";
7
7
  import "../chunk-6HYIRFX2.mjs";
8
- import "../chunk-OJ6O4LSC.mjs";
9
- import "../chunk-KWV3JXSI.mjs";
10
- import "../chunk-TGTPXCLF.mjs";
8
+ import "../chunk-BZ3Y7YV5.mjs";
9
+ import "../chunk-KY3RJ2M3.mjs";
10
+ import "../chunk-BUCULLYM.mjs";
11
11
 
12
12
  // src/presets/node.ts
13
13
  var node_default = [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mswjs/interceptors",
3
3
  "description": "Low-level HTTP/HTTPS/XHR/fetch request interception library.",
4
- "version": "0.32.2",
4
+ "version": "0.33.0",
5
5
  "main": "./lib/node/index.js",
6
6
  "module": "./lib/node/index.mjs",
7
7
  "types": "./lib/node/index.d.ts",
@@ -161,7 +161,7 @@
161
161
  "@open-draft/logger": "^0.3.0",
162
162
  "@open-draft/until": "^2.0.0",
163
163
  "is-node-process": "^1.2.0",
164
- "outvariant": "^1.2.1",
164
+ "outvariant": "^1.4.3",
165
165
  "strict-event-emitter": "^0.5.1"
166
166
  },
167
167
  "resolutions": {
@@ -0,0 +1,7 @@
1
+ export class InterceptorError extends Error {
2
+ constructor(message?: string) {
3
+ super(message)
4
+ this.name = 'InterceptorError'
5
+ Object.setPrototypeOf(this, InterceptorError.prototype)
6
+ }
7
+ }
@@ -4,8 +4,8 @@ import { Interceptor } from './Interceptor'
4
4
  import { BatchInterceptor } from './BatchInterceptor'
5
5
  import { ClientRequestInterceptor } from './interceptors/ClientRequest'
6
6
  import { XMLHttpRequestInterceptor } from './interceptors/XMLHttpRequest'
7
- import { toInteractiveRequest } from './utils/toInteractiveRequest'
8
- import { emitAsync } from './utils/emitAsync'
7
+ import { handleRequest } from './utils/handleRequest'
8
+ import { RequestController } from './RequestController'
9
9
 
10
10
  export interface SerializedRequest {
11
11
  id: string
@@ -46,7 +46,7 @@ export class RemoteHttpInterceptor extends BatchInterceptor<
46
46
 
47
47
  let handleParentMessage: NodeJS.MessageListener
48
48
 
49
- this.on('request', async ({ request, requestId }) => {
49
+ this.on('request', async ({ request, requestId, controller }) => {
50
50
  // Send the stringified intercepted request to
51
51
  // the parent process where the remote resolver is established.
52
52
  const serializedRequest = JSON.stringify({
@@ -64,6 +64,7 @@ export class RemoteHttpInterceptor extends BatchInterceptor<
64
64
  'sent serialized request to the child:',
65
65
  serializedRequest
66
66
  )
67
+
67
68
  process.send?.(`request:${serializedRequest}`)
68
69
 
69
70
  const responsePromise = new Promise<void>((resolve) => {
@@ -90,7 +91,12 @@ export class RemoteHttpInterceptor extends BatchInterceptor<
90
91
  headers: responseInit.headers,
91
92
  })
92
93
 
93
- request.respondWith(mockedResponse)
94
+ /**
95
+ * @todo Support "errorWith" as well.
96
+ * This response handling from the child is incomplete.
97
+ */
98
+
99
+ controller.respondWith(mockedResponse)
94
100
  return resolve()
95
101
  }
96
102
  }
@@ -158,69 +164,68 @@ export class RemoteHttpResolver extends Interceptor<HttpRequestEventMap> {
158
164
  serializedRequest,
159
165
  requestReviver
160
166
  ) as RevivedRequest
167
+
161
168
  logger.info('parsed intercepted request', requestJson)
162
169
 
163
- const capturedRequest = new Request(requestJson.url, {
170
+ const request = new Request(requestJson.url, {
164
171
  method: requestJson.method,
165
172
  headers: new Headers(requestJson.headers),
166
173
  credentials: requestJson.credentials,
167
174
  body: requestJson.body,
168
175
  })
169
176
 
170
- const { interactiveRequest, requestController } =
171
- toInteractiveRequest(capturedRequest)
172
-
173
- this.emitter.once('request', () => {
174
- if (requestController.responsePromise.state === 'pending') {
175
- requestController.respondWith(undefined)
176
- }
177
- })
178
-
179
- await emitAsync(this.emitter, 'request', {
180
- request: interactiveRequest,
177
+ const controller = new RequestController(request)
178
+ await handleRequest({
179
+ request,
181
180
  requestId: requestJson.id,
181
+ controller,
182
+ emitter: this.emitter,
183
+ onResponse: async (response) => {
184
+ this.logger.info('received mocked response!', { response })
185
+
186
+ const responseClone = response.clone()
187
+ const responseText = await responseClone.text()
188
+
189
+ // // Send the mocked response to the child process.
190
+ const serializedResponse = JSON.stringify({
191
+ status: response.status,
192
+ statusText: response.statusText,
193
+ headers: Array.from(response.headers.entries()),
194
+ body: responseText,
195
+ } as SerializedResponse)
196
+
197
+ this.process.send(
198
+ `response:${requestJson.id}:${serializedResponse}`,
199
+ (error) => {
200
+ if (error) {
201
+ return
202
+ }
203
+
204
+ // Emit an optimistic "response" event at this point,
205
+ // not to rely on the back-and-forth signaling for the sake of the event.
206
+ this.emitter.emit('response', {
207
+ request,
208
+ requestId: requestJson.id,
209
+ response: responseClone,
210
+ isMockedResponse: true,
211
+ })
212
+ }
213
+ )
214
+
215
+ logger.info(
216
+ 'sent serialized mocked response to the parent:',
217
+ serializedResponse
218
+ )
219
+ },
220
+ onRequestError: (response) => {
221
+ this.logger.info('received a network error!', { response })
222
+ throw new Error('Not implemented')
223
+ },
224
+ onError: (error) => {
225
+ this.logger.info('request has errored!', { error })
226
+ throw new Error('Not implemented')
227
+ },
182
228
  })
183
-
184
- const mockedResponse = await requestController.responsePromise
185
-
186
- if (!mockedResponse) {
187
- return
188
- }
189
-
190
- logger.info('event.respondWith called with:', mockedResponse)
191
- const responseClone = mockedResponse.clone()
192
- const responseText = await mockedResponse.text()
193
-
194
- // Send the mocked response to the child process.
195
- const serializedResponse = JSON.stringify({
196
- status: mockedResponse.status,
197
- statusText: mockedResponse.statusText,
198
- headers: Array.from(mockedResponse.headers.entries()),
199
- body: responseText,
200
- } as SerializedResponse)
201
-
202
- this.process.send(
203
- `response:${requestJson.id}:${serializedResponse}`,
204
- (error) => {
205
- if (error) {
206
- return
207
- }
208
-
209
- // Emit an optimistic "response" event at this point,
210
- // not to rely on the back-and-forth signaling for the sake of the event.
211
- this.emitter.emit('response', {
212
- response: responseClone,
213
- isMockedResponse: true,
214
- request: capturedRequest,
215
- requestId: requestJson.id,
216
- })
217
- }
218
- )
219
-
220
- logger.info(
221
- 'sent serialized mocked response to the parent:',
222
- serializedResponse
223
- )
224
229
  }
225
230
 
226
231
  this.subscriptions.push(() => {
@@ -0,0 +1,49 @@
1
+ import { it, expect } from 'vitest'
2
+ import { kResponsePromise, RequestController } from './RequestController'
3
+
4
+ it('creates a pending response promise on construction', () => {
5
+ const controller = new RequestController(new Request('http://localhost'))
6
+ expect(controller[kResponsePromise]).toBeInstanceOf(Promise)
7
+ expect(controller[kResponsePromise].state).toBe('pending')
8
+ })
9
+
10
+ it('resolves the response promise with the response provided to "respondWith"', async () => {
11
+ const controller = new RequestController(new Request('http://localhost'))
12
+ controller.respondWith(new Response('hello world'))
13
+
14
+ const response = (await controller[kResponsePromise]) as Response
15
+
16
+ expect(response).toBeInstanceOf(Response)
17
+ expect(response.status).toBe(200)
18
+ expect(await response.text()).toBe('hello world')
19
+ })
20
+
21
+ it('resolves the response promise with the error provided to "errorWith"', async () => {
22
+ const controller = new RequestController(new Request('http://localhost'))
23
+ const error = new Error('Oops!')
24
+ controller.errorWith(error)
25
+
26
+ await expect(controller[kResponsePromise]).resolves.toEqual(error)
27
+ })
28
+
29
+ it('throws when calling "respondWith" multiple times', () => {
30
+ const controller = new RequestController(new Request('http://localhost'))
31
+ controller.respondWith(new Response('hello world'))
32
+
33
+ expect(() => {
34
+ controller.respondWith(new Response('second response'))
35
+ }).toThrow(
36
+ 'Failed to respond to the "GET http://localhost/" request: the "request" event has already been handled.'
37
+ )
38
+ })
39
+
40
+ it('throws when calling "errorWith" multiple times', () => {
41
+ const controller = new RequestController(new Request('http://localhost'))
42
+ controller.errorWith(new Error('Oops!'))
43
+
44
+ expect(() => {
45
+ controller.errorWith(new Error('second error'))
46
+ }).toThrow(
47
+ 'Failed to error the "GET http://localhost/" request: the "request" event has already been handled.'
48
+ )
49
+ })
@@ -0,0 +1,81 @@
1
+ import { invariant } from 'outvariant'
2
+ import { DeferredPromise } from '@open-draft/deferred-promise'
3
+ import { InterceptorError } from './InterceptorError'
4
+
5
+ const kRequestHandled = Symbol('kRequestHandled')
6
+ export const kResponsePromise = Symbol('kResponsePromise')
7
+
8
+ export class RequestController {
9
+ /**
10
+ * Internal response promise.
11
+ * Available only for the library internals to grab the
12
+ * response instance provided by the developer.
13
+ * @note This promise cannot be rejected. It's either infinitely
14
+ * pending or resolved with whichever Response was passed to `respondWith()`.
15
+ */
16
+ [kResponsePromise]: DeferredPromise<Response | Error | undefined>;
17
+
18
+ /**
19
+ * Internal flag indicating if this request has been handled.
20
+ * @note The response promise becomes "fulfilled" on the next tick.
21
+ */
22
+ [kRequestHandled]: boolean
23
+
24
+ constructor(private request: Request) {
25
+ this[kRequestHandled] = false
26
+ this[kResponsePromise] = new DeferredPromise()
27
+ }
28
+
29
+ /**
30
+ * Respond to this request with the given `Response` instance.
31
+ * @example
32
+ * controller.respondWith(new Response())
33
+ * controller.respondWith(Response.json({ id }))
34
+ * controller.respondWith(Response.error())
35
+ */
36
+ public respondWith(response: Response): void {
37
+ invariant.as(
38
+ InterceptorError,
39
+ !this[kRequestHandled],
40
+ 'Failed to respond to the "%s %s" request: the "request" event has already been handled.',
41
+ this.request.method,
42
+ this.request.url
43
+ )
44
+
45
+ this[kRequestHandled] = true
46
+ this[kResponsePromise].resolve(response)
47
+
48
+ /**
49
+ * @note The request conrtoller doesn't do anything
50
+ * apart from letting the interceptor await the response
51
+ * provided by the developer through the response promise.
52
+ * Each interceptor implements the actual respondWith/errorWith
53
+ * logic based on that interceptor's needs.
54
+ */
55
+ }
56
+
57
+ /**
58
+ * Error this request with the given error.
59
+ * @example
60
+ * controller.errorWith()
61
+ * controller.errorWith(new Error('Oops!'))
62
+ */
63
+ public errorWith(error?: Error): void {
64
+ invariant.as(
65
+ InterceptorError,
66
+ !this[kRequestHandled],
67
+ 'Failed to error the "%s %s" request: the "request" event has already been handled.',
68
+ this.request.method,
69
+ this.request.url
70
+ )
71
+
72
+ this[kRequestHandled] = true
73
+
74
+ /**
75
+ * @note Resolve the response promise, not reject.
76
+ * This helps us differentiate between unhandled exceptions
77
+ * and intended errors ("errorWith") while waiting for the response.
78
+ */
79
+ this[kResponsePromise].resolve(error)
80
+ }
81
+ }