@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.
- package/lib/browser/{chunk-F2F5QHHJ.js → chunk-2CRB3JAQ.js} +16 -2
- package/lib/browser/chunk-2CRB3JAQ.js.map +1 -0
- package/lib/browser/{chunk-PXSYFJ7G.mjs → chunk-732REFPX.mjs} +23 -4
- package/lib/browser/chunk-732REFPX.mjs.map +1 -0
- package/lib/browser/{chunk-NIWUC7GF.mjs → chunk-MAEPOYB6.mjs} +67 -35
- package/lib/browser/chunk-MAEPOYB6.mjs.map +1 -0
- package/lib/browser/{chunk-VISYSKLR.mjs → chunk-OMISYKWR.mjs} +16 -2
- package/lib/browser/chunk-OMISYKWR.mjs.map +1 -0
- package/lib/browser/{chunk-LAEV5ZGV.js → chunk-PSX5J3RF.js} +28 -9
- package/lib/browser/chunk-PSX5J3RF.js.map +1 -0
- package/lib/browser/{chunk-RLGVQZ5O.js → chunk-WBHIW62P.js} +69 -37
- package/lib/browser/chunk-WBHIW62P.js.map +1 -0
- package/lib/browser/{glossary-640c9679.d.ts → glossary-1c204f45.d.ts} +11 -0
- package/lib/browser/index.d.ts +1 -1
- package/lib/browser/index.js +2 -2
- package/lib/browser/index.mjs +1 -1
- package/lib/browser/interceptors/XMLHttpRequest/index.d.ts +1 -1
- package/lib/browser/interceptors/XMLHttpRequest/index.js +3 -3
- package/lib/browser/interceptors/XMLHttpRequest/index.mjs +2 -2
- package/lib/browser/interceptors/fetch/index.d.ts +1 -1
- package/lib/browser/interceptors/fetch/index.js +3 -3
- package/lib/browser/interceptors/fetch/index.mjs +2 -2
- package/lib/browser/presets/browser.d.ts +1 -1
- package/lib/browser/presets/browser.js +5 -5
- package/lib/browser/presets/browser.mjs +3 -3
- package/lib/node/{BatchInterceptor-cb145daa.d.ts → BatchInterceptor-2badedde.d.ts} +1 -1
- package/lib/node/{Interceptor-6696a18d.d.ts → Interceptor-88ee47c0.d.ts} +11 -0
- package/lib/node/RemoteHttpInterceptor.d.ts +2 -2
- package/lib/node/RemoteHttpInterceptor.js +9 -10
- package/lib/node/RemoteHttpInterceptor.js.map +1 -1
- package/lib/node/RemoteHttpInterceptor.mjs +5 -6
- package/lib/node/RemoteHttpInterceptor.mjs.map +1 -1
- package/lib/node/{chunk-2SC4AD6S.mjs → chunk-6FRASLM3.mjs} +2 -2
- package/lib/node/{chunk-M4JXH4RP.js → chunk-APT7KA3B.js} +32 -13
- package/lib/node/chunk-APT7KA3B.js.map +1 -0
- package/lib/node/{chunk-KRDNUBDZ.js → chunk-E4AC7YAC.js} +16 -2
- package/lib/node/chunk-E4AC7YAC.js.map +1 -0
- package/lib/node/{chunk-FZJKKO5H.js → chunk-EIBTX65O.js} +1 -1
- package/lib/node/{chunk-FZJKKO5H.js.map → chunk-EIBTX65O.js.map} +1 -1
- package/lib/node/{chunk-UXEUSYDY.js → chunk-HAIWBQD5.js} +48 -49
- package/lib/node/chunk-HAIWBQD5.js.map +1 -0
- package/lib/node/{chunk-L576JLIX.mjs → chunk-JMNEFEYU.mjs} +43 -44
- package/lib/node/chunk-JMNEFEYU.mjs.map +1 -0
- package/lib/node/{chunk-KGNKRQ7B.mjs → chunk-KSHIDGUL.mjs} +24 -5
- package/lib/node/chunk-KSHIDGUL.mjs.map +1 -0
- package/lib/node/{chunk-Z2DPXZWN.js → chunk-LTEXDYJ6.js} +3 -3
- package/lib/node/{chunk-HAGW22AN.mjs → chunk-OJ6O4LSC.mjs} +1 -1
- package/lib/node/{chunk-HAGW22AN.mjs.map → chunk-OJ6O4LSC.mjs.map} +1 -1
- package/lib/node/{chunk-DQ5DO3KN.mjs → chunk-Q7POAM5N.mjs} +16 -2
- package/lib/node/chunk-Q7POAM5N.mjs.map +1 -0
- package/lib/node/index.d.ts +2 -2
- package/lib/node/index.js +4 -4
- package/lib/node/index.mjs +3 -3
- package/lib/node/interceptors/ClientRequest/index.d.ts +1 -1
- package/lib/node/interceptors/ClientRequest/index.js +3 -4
- package/lib/node/interceptors/ClientRequest/index.mjs +2 -3
- package/lib/node/interceptors/XMLHttpRequest/index.d.ts +1 -1
- package/lib/node/interceptors/XMLHttpRequest/index.js +4 -4
- package/lib/node/interceptors/XMLHttpRequest/index.mjs +3 -3
- package/lib/node/interceptors/fetch/index.d.ts +1 -1
- package/lib/node/interceptors/fetch/index.js +71 -32
- package/lib/node/interceptors/fetch/index.js.map +1 -1
- package/lib/node/interceptors/fetch/index.mjs +67 -28
- package/lib/node/interceptors/fetch/index.mjs.map +1 -1
- package/lib/node/presets/node.d.ts +1 -1
- package/lib/node/presets/node.js +6 -7
- package/lib/node/presets/node.js.map +1 -1
- package/lib/node/presets/node.mjs +4 -5
- package/lib/node/presets/node.mjs.map +1 -1
- package/package.json +1 -1
- package/src/glossary.ts +11 -0
- package/src/interceptors/ClientRequest/NodeClientRequest.ts +46 -18
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts +4 -7
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +36 -3
- package/src/interceptors/fetch/index.ts +88 -33
- package/src/utils/getUrlByRequestOptions.test.ts +31 -3
- package/src/utils/getUrlByRequestOptions.ts +14 -38
- package/src/utils/isObject.test.ts +4 -3
- package/src/utils/isObject.ts +4 -2
- package/src/utils/responseUtils.ts +16 -0
- package/lib/browser/chunk-F2F5QHHJ.js.map +0 -1
- package/lib/browser/chunk-LAEV5ZGV.js.map +0 -1
- package/lib/browser/chunk-NIWUC7GF.mjs.map +0 -1
- package/lib/browser/chunk-PXSYFJ7G.mjs.map +0 -1
- package/lib/browser/chunk-RLGVQZ5O.js.map +0 -1
- package/lib/browser/chunk-VISYSKLR.mjs.map +0 -1
- package/lib/node/chunk-DERTLGL3.mjs +0 -14
- package/lib/node/chunk-DERTLGL3.mjs.map +0 -1
- package/lib/node/chunk-DQ5DO3KN.mjs.map +0 -1
- package/lib/node/chunk-KGNKRQ7B.mjs.map +0 -1
- package/lib/node/chunk-KRDNUBDZ.js.map +0 -1
- package/lib/node/chunk-L576JLIX.mjs.map +0 -1
- package/lib/node/chunk-M4JXH4RP.js.map +0 -1
- package/lib/node/chunk-UXEUSYDY.js.map +0 -1
- package/lib/node/chunk-Y6GRL6UD.js +0 -14
- package/lib/node/chunk-Y6GRL6UD.js.map +0 -1
- /package/lib/node/{chunk-2SC4AD6S.mjs.map → chunk-6FRASLM3.mjs.map} +0 -0
- /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 {
|
|
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
|
-
//
|
|
272
|
+
// Handle thrown Response instances.
|
|
271
273
|
if (resolverResult.error instanceof Response) {
|
|
272
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
+
errorWith(createNetworkError(mockedResponse))
|
|
225
|
+
} else {
|
|
226
|
+
respondWith(mockedResponse)
|
|
177
227
|
}
|
|
178
228
|
|
|
179
|
-
return
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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('
|
|
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
|
|
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('
|
|
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
|
|
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(
|
|
113
|
-
|
|
92
|
+
function getHostname(options: ResolvedRequestOptions): string | undefined {
|
|
93
|
+
let host = options.hostname || options.host
|
|
114
94
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (isRawIPv6Address(host)) {
|
|
120
|
-
return `[${host}]${portString}`
|
|
121
|
-
}
|
|
95
|
+
if (host) {
|
|
96
|
+
if (isRawIPv6Address(host)) {
|
|
97
|
+
host = `[${host}]`
|
|
98
|
+
}
|
|
122
99
|
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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(
|
|
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
|
|
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('
|
|
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('
|
|
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('
|
|
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)
|
package/src/utils/isObject.ts
CHANGED
|
@@ -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
|
|
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"]}
|