@mswjs/interceptors 0.28.3 → 0.29.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 (98) hide show
  1. package/lib/browser/{chunk-F2F5QHHJ.js → chunk-2CRB3JAQ.js} +16 -2
  2. package/lib/browser/chunk-2CRB3JAQ.js.map +1 -0
  3. package/lib/browser/{chunk-PXSYFJ7G.mjs → chunk-732REFPX.mjs} +23 -4
  4. package/lib/browser/chunk-732REFPX.mjs.map +1 -0
  5. package/lib/browser/{chunk-NIWUC7GF.mjs → chunk-MAEPOYB6.mjs} +67 -35
  6. package/lib/browser/chunk-MAEPOYB6.mjs.map +1 -0
  7. package/lib/browser/{chunk-VISYSKLR.mjs → chunk-OMISYKWR.mjs} +16 -2
  8. package/lib/browser/chunk-OMISYKWR.mjs.map +1 -0
  9. package/lib/browser/{chunk-LAEV5ZGV.js → chunk-PSX5J3RF.js} +28 -9
  10. package/lib/browser/chunk-PSX5J3RF.js.map +1 -0
  11. package/lib/browser/{chunk-RLGVQZ5O.js → chunk-WBHIW62P.js} +69 -37
  12. package/lib/browser/chunk-WBHIW62P.js.map +1 -0
  13. package/lib/browser/{glossary-640c9679.d.ts → glossary-1c204f45.d.ts} +11 -0
  14. package/lib/browser/index.d.ts +1 -1
  15. package/lib/browser/index.js +2 -2
  16. package/lib/browser/index.mjs +1 -1
  17. package/lib/browser/interceptors/XMLHttpRequest/index.d.ts +1 -1
  18. package/lib/browser/interceptors/XMLHttpRequest/index.js +3 -3
  19. package/lib/browser/interceptors/XMLHttpRequest/index.mjs +2 -2
  20. package/lib/browser/interceptors/fetch/index.d.ts +1 -1
  21. package/lib/browser/interceptors/fetch/index.js +3 -3
  22. package/lib/browser/interceptors/fetch/index.mjs +2 -2
  23. package/lib/browser/presets/browser.d.ts +1 -1
  24. package/lib/browser/presets/browser.js +5 -5
  25. package/lib/browser/presets/browser.mjs +3 -3
  26. package/lib/node/{BatchInterceptor-cb145daa.d.ts → BatchInterceptor-2badedde.d.ts} +1 -1
  27. package/lib/node/{Interceptor-6696a18d.d.ts → Interceptor-88ee47c0.d.ts} +11 -0
  28. package/lib/node/RemoteHttpInterceptor.d.ts +2 -2
  29. package/lib/node/RemoteHttpInterceptor.js +9 -10
  30. package/lib/node/RemoteHttpInterceptor.js.map +1 -1
  31. package/lib/node/RemoteHttpInterceptor.mjs +5 -6
  32. package/lib/node/RemoteHttpInterceptor.mjs.map +1 -1
  33. package/lib/node/{chunk-2SC4AD6S.mjs → chunk-6FRASLM3.mjs} +2 -2
  34. package/lib/node/{chunk-M4JXH4RP.js → chunk-APT7KA3B.js} +32 -13
  35. package/lib/node/chunk-APT7KA3B.js.map +1 -0
  36. package/lib/node/{chunk-KRDNUBDZ.js → chunk-E4AC7YAC.js} +16 -2
  37. package/lib/node/chunk-E4AC7YAC.js.map +1 -0
  38. package/lib/node/{chunk-FZJKKO5H.js → chunk-EIBTX65O.js} +1 -1
  39. package/lib/node/{chunk-FZJKKO5H.js.map → chunk-EIBTX65O.js.map} +1 -1
  40. package/lib/node/{chunk-UXEUSYDY.js → chunk-HAIWBQD5.js} +48 -49
  41. package/lib/node/chunk-HAIWBQD5.js.map +1 -0
  42. package/lib/node/{chunk-L576JLIX.mjs → chunk-JMNEFEYU.mjs} +43 -44
  43. package/lib/node/chunk-JMNEFEYU.mjs.map +1 -0
  44. package/lib/node/{chunk-KGNKRQ7B.mjs → chunk-KSHIDGUL.mjs} +24 -5
  45. package/lib/node/chunk-KSHIDGUL.mjs.map +1 -0
  46. package/lib/node/{chunk-Z2DPXZWN.js → chunk-LTEXDYJ6.js} +3 -3
  47. package/lib/node/{chunk-HAGW22AN.mjs → chunk-OJ6O4LSC.mjs} +1 -1
  48. package/lib/node/{chunk-HAGW22AN.mjs.map → chunk-OJ6O4LSC.mjs.map} +1 -1
  49. package/lib/node/{chunk-DQ5DO3KN.mjs → chunk-Q7POAM5N.mjs} +16 -2
  50. package/lib/node/chunk-Q7POAM5N.mjs.map +1 -0
  51. package/lib/node/index.d.ts +2 -2
  52. package/lib/node/index.js +4 -4
  53. package/lib/node/index.mjs +3 -3
  54. package/lib/node/interceptors/ClientRequest/index.d.ts +1 -1
  55. package/lib/node/interceptors/ClientRequest/index.js +3 -4
  56. package/lib/node/interceptors/ClientRequest/index.mjs +2 -3
  57. package/lib/node/interceptors/XMLHttpRequest/index.d.ts +1 -1
  58. package/lib/node/interceptors/XMLHttpRequest/index.js +4 -4
  59. package/lib/node/interceptors/XMLHttpRequest/index.mjs +3 -3
  60. package/lib/node/interceptors/fetch/index.d.ts +1 -1
  61. package/lib/node/interceptors/fetch/index.js +71 -32
  62. package/lib/node/interceptors/fetch/index.js.map +1 -1
  63. package/lib/node/interceptors/fetch/index.mjs +67 -28
  64. package/lib/node/interceptors/fetch/index.mjs.map +1 -1
  65. package/lib/node/presets/node.d.ts +1 -1
  66. package/lib/node/presets/node.js +6 -7
  67. package/lib/node/presets/node.js.map +1 -1
  68. package/lib/node/presets/node.mjs +4 -5
  69. package/lib/node/presets/node.mjs.map +1 -1
  70. package/package.json +1 -1
  71. package/src/glossary.ts +11 -0
  72. package/src/interceptors/ClientRequest/NodeClientRequest.ts +46 -18
  73. package/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts +4 -7
  74. package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +36 -3
  75. package/src/interceptors/fetch/index.ts +88 -33
  76. package/src/utils/getUrlByRequestOptions.test.ts +31 -3
  77. package/src/utils/getUrlByRequestOptions.ts +14 -38
  78. package/src/utils/isObject.test.ts +4 -3
  79. package/src/utils/isObject.ts +4 -2
  80. package/src/utils/responseUtils.ts +16 -0
  81. package/lib/browser/chunk-F2F5QHHJ.js.map +0 -1
  82. package/lib/browser/chunk-LAEV5ZGV.js.map +0 -1
  83. package/lib/browser/chunk-NIWUC7GF.mjs.map +0 -1
  84. package/lib/browser/chunk-PXSYFJ7G.mjs.map +0 -1
  85. package/lib/browser/chunk-RLGVQZ5O.js.map +0 -1
  86. package/lib/browser/chunk-VISYSKLR.mjs.map +0 -1
  87. package/lib/node/chunk-DERTLGL3.mjs +0 -14
  88. package/lib/node/chunk-DERTLGL3.mjs.map +0 -1
  89. package/lib/node/chunk-DQ5DO3KN.mjs.map +0 -1
  90. package/lib/node/chunk-KGNKRQ7B.mjs.map +0 -1
  91. package/lib/node/chunk-KRDNUBDZ.js.map +0 -1
  92. package/lib/node/chunk-L576JLIX.mjs.map +0 -1
  93. package/lib/node/chunk-M4JXH4RP.js.map +0 -1
  94. package/lib/node/chunk-UXEUSYDY.js.map +0 -1
  95. package/lib/node/chunk-Y6GRL6UD.js +0 -14
  96. package/lib/node/chunk-Y6GRL6UD.js.map +0 -1
  97. /package/lib/node/{chunk-2SC4AD6S.mjs.map → chunk-6FRASLM3.mjs.map} +0 -0
  98. /package/lib/node/{chunk-Z2DPXZWN.js.map → chunk-LTEXDYJ6.js.map} +0 -0
@@ -19,11 +19,13 @@ import { createRequest } from './utils/createRequest'
19
19
  import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
20
20
  import { emitAsync } from '../../utils/emitAsync'
21
21
  import { getRawFetchHeaders } from '../../utils/getRawFetchHeaders'
22
- import { isPropertyAccessible } from '../../utils/isPropertyAccessible'
23
22
  import { isNodeLikeError } from '../../utils/isNodeLikeError'
24
23
  import { INTERNAL_REQUEST_ID_HEADER_NAME } from '../../Interceptor'
25
24
  import { createRequestId } from '../../createRequestId'
26
- import { createServerErrorResponse } from '../../utils/responseUtils'
25
+ import {
26
+ createServerErrorResponse,
27
+ isResponseError,
28
+ } from '../../utils/responseUtils'
27
29
 
28
30
  export type Protocol = 'http' | 'https'
29
31
 
@@ -267,9 +269,20 @@ export class NodeClientRequest extends ClientRequest {
267
269
  resolverResult.error
268
270
  )
269
271
 
270
- // Treat thrown Responses as mocked responses.
272
+ // Handle thrown Response instances.
271
273
  if (resolverResult.error instanceof Response) {
272
- this.respondWith(resolverResult.error)
274
+ // Treat thrown Response.error() as a request error.
275
+ if (isResponseError(resolverResult.error)) {
276
+ this.logger.info(
277
+ 'received network error response, erroring request...'
278
+ )
279
+
280
+ this.errorWith(new TypeError('Network error'))
281
+ } else {
282
+ // Handle a thrown Response as a mocked response.
283
+ this.respondWith(resolverResult.error)
284
+ }
285
+
273
286
  return
274
287
  }
275
288
 
@@ -280,10 +293,34 @@ export class NodeClientRequest extends ClientRequest {
280
293
  return this
281
294
  }
282
295
 
283
- // Unhandled exceptions in the request listeners are
284
- // synonymous to unhandled exceptions on the server.
285
- // Those are represented as 500 error responses.
286
- this.respondWith(createServerErrorResponse(resolverResult.error))
296
+ until(async () => {
297
+ if (this.emitter.listenerCount('unhandledException') > 0) {
298
+ // Emit the "unhandledException" event to allow the client
299
+ // to opt-out from the default handling of exceptions
300
+ // as 500 error responses.
301
+ await emitAsync(this.emitter, 'unhandledException', {
302
+ error: resolverResult.error,
303
+ request: capturedRequest,
304
+ requestId,
305
+ controller: {
306
+ respondWith: this.respondWith.bind(this),
307
+ errorWith: this.errorWith.bind(this),
308
+ },
309
+ })
310
+
311
+ // If after the "unhandledException" listeners are done,
312
+ // the request is either not writable (was mocked) or
313
+ // destroyed (has errored), do nothing.
314
+ if (this.writableEnded || this.destroyed) {
315
+ return
316
+ }
317
+ }
318
+
319
+ // Unhandled exceptions in the request listeners are
320
+ // synonymous to unhandled exceptions on the server.
321
+ // Those are represented as 500 error responses.
322
+ this.respondWith(createServerErrorResponse(resolverResult.error))
323
+ })
287
324
 
288
325
  return this
289
326
  }
@@ -304,16 +341,7 @@ export class NodeClientRequest extends ClientRequest {
304
341
  this.destroyed = false
305
342
 
306
343
  // Handle mocked "Response.error" network error responses.
307
- if (
308
- /**
309
- * @note Some environments, like Miniflare (Cloudflare) do not
310
- * implement the "Response.type" property and throw on its access.
311
- * Safely check if we can access "type" on "Response" before continuing.
312
- * @see https://github.com/mswjs/msw/issues/1834
313
- */
314
- isPropertyAccessible(mockedResponse, 'type') &&
315
- mockedResponse.type === 'error'
316
- ) {
344
+ if (isResponseError(mockedResponse)) {
317
345
  this.logger.info(
318
346
  'received network error response, erroring request...'
319
347
  )
@@ -49,10 +49,7 @@ export class XMLHttpRequestController {
49
49
  private responseBuffer: Uint8Array
50
50
  private events: Map<keyof XMLHttpRequestEventTargetEventMap, Array<Function>>
51
51
 
52
- constructor(
53
- readonly initialRequest: XMLHttpRequest,
54
- public logger: Logger
55
- ) {
52
+ constructor(readonly initialRequest: XMLHttpRequest, public logger: Logger) {
56
53
  this.events = new Map()
57
54
  this.requestId = createRequestId()
58
55
  this.requestHeaders = new Headers()
@@ -103,7 +100,7 @@ export class XMLHttpRequestController {
103
100
  case 'addEventListener': {
104
101
  const [eventName, listener] = args as [
105
102
  keyof XMLHttpRequestEventTargetEventMap,
106
- Function,
103
+ Function
107
104
  ]
108
105
 
109
106
  this.registerEvent(eventName, listener)
@@ -123,7 +120,7 @@ export class XMLHttpRequestController {
123
120
 
124
121
  case 'send': {
125
122
  const [body] = args as [
126
- body?: XMLHttpRequestBodyInit | Document | null,
123
+ body?: XMLHttpRequestBodyInit | Document | null
127
124
  ]
128
125
 
129
126
  if (body != null) {
@@ -524,7 +521,7 @@ export class XMLHttpRequestController {
524
521
  private trigger<
525
522
  EventName extends keyof (XMLHttpRequestEventTargetEventMap & {
526
523
  readystatechange: ProgressEvent<XMLHttpRequestEventTarget>
527
- }),
524
+ })
528
525
  >(eventName: EventName, options?: ProgressEventInit): void {
529
526
  const callback = this.request[`on${eventName}`]
530
527
  const event = createEvent(this.request, eventName, options)
@@ -4,7 +4,10 @@ import { XMLHttpRequestEmitter } from '.'
4
4
  import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
5
5
  import { emitAsync } from '../../utils/emitAsync'
6
6
  import { XMLHttpRequestController } from './XMLHttpRequestController'
7
- import { createServerErrorResponse } from '../../utils/responseUtils'
7
+ import {
8
+ createServerErrorResponse,
9
+ isResponseError,
10
+ } from '../../utils/responseUtils'
8
11
 
9
12
  export interface XMLHttpRequestProxyOptions {
10
13
  emitter: XMLHttpRequestEmitter
@@ -97,9 +100,39 @@ export function createXMLHttpRequestProxy({
97
100
 
98
101
  // Treat thrown Responses as mocked responses.
99
102
  if (resolverResult.error instanceof Response) {
100
- this.respondWith(resolverResult.error)
103
+ if (isResponseError(resolverResult.error)) {
104
+ xhrRequestController.errorWith(new TypeError('Network error'))
105
+ } else {
106
+ this.respondWith(resolverResult.error)
107
+ }
108
+
101
109
  return
102
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
+ })
126
+
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
133
+ }
134
+ }
135
+
103
136
  // Unhandled exceptions in the request listeners are
104
137
  // synonymous to unhandled exceptions on the server.
105
138
  // Those are represented as 500 error responses.
@@ -119,7 +152,7 @@ export function createXMLHttpRequestProxy({
119
152
  mockedResponse.statusText
120
153
  )
121
154
 
122
- if (mockedResponse.type === 'error') {
155
+ if (isResponseError(mockedResponse)) {
123
156
  this.logger.info(
124
157
  'received a network error response, rejecting the request promise...'
125
158
  )
@@ -5,10 +5,12 @@ import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
5
5
  import { Interceptor } from '../../Interceptor'
6
6
  import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
7
7
  import { emitAsync } from '../../utils/emitAsync'
8
- import { isPropertyAccessible } from '../../utils/isPropertyAccessible'
9
8
  import { canParseUrl } from '../../utils/canParseUrl'
10
9
  import { createRequestId } from '../../createRequestId'
11
- import { createServerErrorResponse } from '../../utils/responseUtils'
10
+ import {
11
+ createServerErrorResponse,
12
+ isResponseError,
13
+ } from '../../utils/responseUtils'
12
14
 
13
15
  export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
14
16
  static symbol = Symbol('fetch')
@@ -86,18 +88,26 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
86
88
  )
87
89
  }
88
90
 
89
- const respondWith = (response: Response): Response => {
90
- // Clone the mocked response for the "response" event listener.
91
- // This way, the listener can read the response and not lock its body
92
- // for the actual fetch consumer.
93
- const responseClone = response.clone()
94
-
95
- this.emitter.emit('response', {
96
- response: responseClone,
97
- isMockedResponse: true,
98
- request: interactiveRequest,
99
- requestId,
100
- })
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
+ }
101
111
 
102
112
  // Set the "response.url" property to equal the intercepted request URL.
103
113
  Object.defineProperty(response, 'url', {
@@ -107,7 +117,11 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
107
117
  value: request.url,
108
118
  })
109
119
 
110
- return response
120
+ responsePromise.resolve(response)
121
+ }
122
+
123
+ const errorWith = (reason: unknown): void => {
124
+ responsePromise.reject(reason)
111
125
  }
112
126
 
113
127
  const resolverResult = await until<unknown, Response | undefined>(
@@ -136,19 +150,56 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
136
150
  )
137
151
 
138
152
  if (requestAborted.state === 'rejected') {
139
- return Promise.reject(requestAborted.rejectionReason)
153
+ this.logger.info(
154
+ 'request has been aborted:',
155
+ requestAborted.rejectionReason
156
+ )
157
+
158
+ responsePromise.reject(requestAborted.rejectionReason)
159
+ return responsePromise
140
160
  }
141
161
 
142
162
  if (resolverResult.error) {
163
+ this.logger.info(
164
+ 'request listerner threw an error:',
165
+ resolverResult.error
166
+ )
167
+
143
168
  // Treat thrown Responses as mocked responses.
144
169
  if (resolverResult.error instanceof Response) {
145
- return respondWith(resolverResult.error)
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)
176
+ }
177
+ }
178
+
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
+ },
191
+ })
192
+
193
+ if (responsePromise.state !== 'pending') {
194
+ return responsePromise
195
+ }
146
196
  }
147
197
 
148
198
  // Unhandled exceptions in the request listeners are
149
199
  // synonymous to unhandled exceptions on the server.
150
200
  // Those are represented as 500 error responses.
151
- return createServerErrorResponse(resolverResult.error)
201
+ respondWith(createServerErrorResponse(resolverResult.error))
202
+ return responsePromise
152
203
  }
153
204
 
154
205
  const mockedResponse = resolverResult.data
@@ -157,10 +208,7 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
157
208
  this.logger.info('received mocked response:', mockedResponse)
158
209
 
159
210
  // Reject the request Promise on mocked "Response.error" responses.
160
- if (
161
- isPropertyAccessible(mockedResponse, 'type') &&
162
- mockedResponse.type === 'error'
163
- ) {
211
+ if (isResponseError(mockedResponse)) {
164
212
  this.logger.info(
165
213
  'received a network error response, rejecting the request promise...'
166
214
  )
@@ -173,24 +221,31 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
173
221
  * "response.error" will equal to undefined, making "cause" an empty Error.
174
222
  * @see https://github.com/nodejs/undici/blob/83cb522ae0157a19d149d72c7d03d46e34510d0a/lib/fetch/response.js#L344
175
223
  */
176
- return Promise.reject(createNetworkError(mockedResponse))
224
+ errorWith(createNetworkError(mockedResponse))
225
+ } else {
226
+ respondWith(mockedResponse)
177
227
  }
178
228
 
179
- return respondWith(mockedResponse)
229
+ return responsePromise
180
230
  }
181
231
 
182
232
  this.logger.info('no mocked response received!')
183
233
 
184
234
  return pureFetch(request).then((response) => {
185
- const responseClone = response.clone()
186
- this.logger.info('original fetch performed', responseClone)
187
-
188
- this.emitter.emit('response', {
189
- response: responseClone,
190
- isMockedResponse: false,
191
- request: interactiveRequest,
192
- requestId,
193
- })
235
+ this.logger.info('original fetch performed', response)
236
+
237
+ if (this.emitter.listenerCount('response') > 0) {
238
+ this.logger.info('emitting the "response" event...')
239
+
240
+ const responseClone = response.clone()
241
+
242
+ this.emitter.emit('response', {
243
+ response: responseClone,
244
+ isMockedResponse: false,
245
+ request: interactiveRequest,
246
+ requestId,
247
+ })
248
+ }
194
249
 
195
250
  return response
196
251
  })
@@ -105,10 +105,15 @@ it('resolves hostname to localhost if none provided', () => {
105
105
  expect(getUrlByRequestOptions({}).hostname).toBe('localhost')
106
106
  })
107
107
 
108
- it('supports "hostname" instead of "host" and "port"', () => {
108
+ it('resolves host to localhost if none provided', () => {
109
+ expect(getUrlByRequestOptions({}).host).toBe('localhost')
110
+ })
111
+
112
+ it('supports "hostname" and "port"', () => {
109
113
  const options: RequestOptions = {
110
114
  protocol: 'https:',
111
- hostname: '127.0.0.1:1234',
115
+ hostname: '127.0.0.1',
116
+ port: 1234,
112
117
  path: '/resource',
113
118
  }
114
119
 
@@ -117,7 +122,20 @@ it('supports "hostname" instead of "host" and "port"', () => {
117
122
  )
118
123
  })
119
124
 
120
- it('handles IPv6 hostnames', () => {
125
+ it('use "hostname" if both "hostname" and "host" are specified', () => {
126
+ const options: RequestOptions = {
127
+ protocol: 'https:',
128
+ host: 'host',
129
+ hostname: 'hostname',
130
+ path: '/resource',
131
+ }
132
+
133
+ expect(getUrlByRequestOptions(options).href).toBe(
134
+ 'https://hostname/resource'
135
+ )
136
+ })
137
+
138
+ it('parses "host" in IPv6', () => {
121
139
  expect(
122
140
  getUrlByRequestOptions({
123
141
  host: '::1',
@@ -125,6 +143,16 @@ it('handles IPv6 hostnames', () => {
125
143
  }).href
126
144
  ).toBe('http://[::1]/resource')
127
145
 
146
+ expect(
147
+ getUrlByRequestOptions({
148
+ host: '[::1]',
149
+ path: '/resource',
150
+ }).href
151
+ ).toBe('http://[::1]/resource')
152
+
153
+ })
154
+
155
+ it('parses "host" and "port" in IPv6', () => {
128
156
  expect(
129
157
  getUrlByRequestOptions({
130
158
  host: '::1',
@@ -15,7 +15,7 @@ export type ResolvedRequestOptions = RequestOptions & RequestSelf
15
15
 
16
16
  export const DEFAULT_PATH = '/'
17
17
  const DEFAULT_PROTOCOL = 'http:'
18
- const DEFAULT_HOST = 'localhost'
18
+ const DEFAULT_HOSTNAME = 'localhost'
19
19
  const SSL_PORT = 443
20
20
 
21
21
  function getAgent(
@@ -50,15 +50,6 @@ function getPortByRequestOptions(
50
50
  return Number(options.port)
51
51
  }
52
52
 
53
- // Extract the port from the hostname.
54
- if (options.hostname != null) {
55
- const [, extractedPort] = options.hostname.match(/:(\d+)$/) || []
56
-
57
- if (extractedPort != null) {
58
- return Number(extractedPort)
59
- }
60
- }
61
-
62
53
  // Otherwise, try to resolve port from the agent.
63
54
  const agent = getAgent(options)
64
55
 
@@ -75,17 +66,6 @@ function getPortByRequestOptions(
75
66
  return undefined
76
67
  }
77
68
 
78
- function getHostByRequestOptions(options: ResolvedRequestOptions): string {
79
- const { hostname, host } = options
80
-
81
- // If the hostname is specified, resolve the host from the "host:port" string.
82
- if (hostname != null) {
83
- return hostname.replace(/:\d+$/, '')
84
- }
85
-
86
- return host || DEFAULT_HOST
87
- }
88
-
89
69
  interface RequestAuth {
90
70
  username: string
91
71
  password: string
@@ -109,22 +89,20 @@ function isRawIPv6Address(host: string): boolean {
109
89
  return host.includes(':') && !host.startsWith('[') && !host.endsWith(']')
110
90
  }
111
91
 
112
- function getHostname(host: string, port?: number): string {
113
- const portString = typeof port !== 'undefined' ? `:${port}` : ''
92
+ function getHostname(options: ResolvedRequestOptions): string | undefined {
93
+ let host = options.hostname || options.host
114
94
 
115
- /**
116
- * @note As of Node >= 17, hosts (including "localhost") can resolve to IPv6
117
- * addresses, so construct valid URL by surrounding the IPv6 host with brackets.
118
- */
119
- if (isRawIPv6Address(host)) {
120
- return `[${host}]${portString}`
121
- }
95
+ if (host) {
96
+ if (isRawIPv6Address(host)) {
97
+ host = `[${host}]`
98
+ }
122
99
 
123
- if (typeof port === 'undefined') {
124
- return host
100
+ // Check the presence of the port, and if it's present,
101
+ // remove it from the host, returning a hostname.
102
+ return new URL(`http://${host}`).hostname
125
103
  }
126
104
 
127
- return `${host}${portString}`
105
+ return DEFAULT_HOSTNAME
128
106
  }
129
107
 
130
108
  /**
@@ -146,13 +124,10 @@ export function getUrlByRequestOptions(options: ResolvedRequestOptions): URL {
146
124
  const protocol = getProtocolByRequestOptions(options)
147
125
  logger.info('protocol', protocol)
148
126
 
149
- const host = getHostByRequestOptions(options)
150
- logger.info('host', host)
151
-
152
127
  const port = getPortByRequestOptions(options)
153
128
  logger.info('port', port)
154
129
 
155
- const hostname = getHostname(host, port)
130
+ const hostname = getHostname(options)
156
131
  logger.info('hostname', hostname)
157
132
 
158
133
  const path = options.path || DEFAULT_PATH
@@ -166,7 +141,8 @@ export function getUrlByRequestOptions(options: ResolvedRequestOptions): URL {
166
141
  : ''
167
142
  logger.info('auth string:', authString)
168
143
 
169
- const url = new URL(`${protocol}//${hostname}${path}`)
144
+ const portString = typeof port !== 'undefined' ? `:${port}` : ''
145
+ const url = new URL(`${protocol}//${hostname}${portString}${path}`)
170
146
  url.username = credentials?.username || ''
171
147
  url.password = credentials?.password || ''
172
148
 
@@ -1,17 +1,18 @@
1
1
  import { it, expect } from 'vitest'
2
2
  import { isObject } from './isObject'
3
3
 
4
- it('resolves given an object', () => {
4
+ it('returns true given an object', () => {
5
5
  expect(isObject({})).toBe(true)
6
6
  expect(isObject({ a: 1 })).toBe(true)
7
7
  })
8
8
 
9
- it('rejects given an object-like instance', () => {
9
+ it('returns false given an object-like instance', () => {
10
10
  expect(isObject([1])).toBe(false)
11
11
  expect(isObject(function () {})).toBe(false)
12
+ expect(isObject(new Response())).toBe(false)
12
13
  })
13
14
 
14
- it('rejects given a non-object instance', () => {
15
+ it('returns false given a non-object instance', () => {
15
16
  expect(isObject(null)).toBe(false)
16
17
  expect(isObject(undefined)).toBe(false)
17
18
  expect(isObject(false)).toBe(false)
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Determines if a given value is an instance of object.
3
3
  */
4
- export function isObject<T>(value: any): value is T {
5
- return Object.prototype.toString.call(value) === '[object Object]'
4
+ export function isObject<T>(value: any, loose = false): value is T {
5
+ return loose
6
+ ? Object.prototype.toString.call(value).startsWith('[object ')
7
+ : Object.prototype.toString.call(value) === '[object Object]'
6
8
  }
@@ -1,3 +1,5 @@
1
+ import { isPropertyAccessible } from './isPropertyAccessible'
2
+
1
3
  /**
2
4
  * Response status codes for responses that cannot have body.
3
5
  * @see https://fetch.spec.whatwg.org/#statuses
@@ -37,3 +39,17 @@ export function createServerErrorResponse(body: unknown): Response {
37
39
  }
38
40
  )
39
41
  }
42
+
43
+ /**
44
+ * Checks if the given response is a `Response.error()`.
45
+ *
46
+ * @note Some environments, like Miniflare (Cloudflare) do not
47
+ * implement the "Response.type" property and throw on its access.
48
+ * Safely check if we can access "type" on "Response" before continuing.
49
+ * @see https://github.com/mswjs/msw/issues/1834
50
+ */
51
+ export function isResponseError(
52
+ response: Response
53
+ ): response is Response & { type: 'error' } {
54
+ return isPropertyAccessible(response, 'type') && response.type === 'error'
55
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/glossary.ts","../../src/utils/responseUtils.ts"],"names":[],"mappings":";AAEO,IAAM,oBAAmC,OAAO,iBAAiB;;;ACEjE,IAAM,qCAAqC,oBAAI,IAAI;AAAA,EACxD;AAAA,EAAK;AAAA,EAAK;AAAA,EAAK;AAAA,EAAK;AACtB,CAAC;AAMM,SAAS,sBAAsB,QAAyB;AAC7D,SAAO,mCAAmC,IAAI,MAAM;AACtD;AAKO,SAAS,0BAA0B,MAAyB;AACjE,SAAO,IAAI;AAAA,IACT,KAAK;AAAA,MACH,gBAAgB,QACZ;AAAA,QACE,MAAM,KAAK;AAAA,QACX,SAAS,KAAK;AAAA,QACd,OAAO,KAAK;AAAA,MACd,IACA;AAAA,IACN;AAAA,IACA;AAAA,MACE,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACF","sourcesContent":["import type { InteractiveRequest } from './utils/toInteractiveRequest'\n\nexport const IS_PATCHED_MODULE: unique symbol = Symbol('isPatchedModule')\n\nexport type RequestCredentials = 'omit' | 'include' | 'same-origin'\n\nexport type HttpRequestEventMap = {\n request: [\n args: {\n request: InteractiveRequest\n requestId: string\n }\n ]\n response: [\n args: {\n response: Response\n isMockedResponse: boolean\n request: Request\n requestId: string\n }\n ]\n}\n","/**\n * Response status codes for responses that cannot have body.\n * @see https://fetch.spec.whatwg.org/#statuses\n */\nexport const RESPONSE_STATUS_CODES_WITHOUT_BODY = new Set([\n 101, 103, 204, 205, 304,\n])\n\n/**\n * Returns a boolean indicating whether the given response status\n * code represents a response that cannot have a body.\n */\nexport function isResponseWithoutBody(status: number): boolean {\n return RESPONSE_STATUS_CODES_WITHOUT_BODY.has(status)\n}\n\n/**\n * Creates a generic 500 Unhandled Exception response.\n */\nexport function createServerErrorResponse(body: unknown): Response {\n return new Response(\n JSON.stringify(\n body instanceof Error\n ? {\n name: body.name,\n message: body.message,\n stack: body.stack,\n }\n : body\n ),\n {\n status: 500,\n statusText: 'Unhandled Exception',\n headers: {\n 'Content-Type': 'application/json',\n },\n }\n )\n}\n"]}