@mswjs/interceptors 0.32.1 → 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.
- package/README.md +35 -7
- package/lib/browser/{chunk-732REFPX.mjs → chunk-5ETVT6GU.mjs} +28 -79
- package/lib/browser/chunk-5ETVT6GU.mjs.map +1 -0
- package/lib/browser/chunk-6MBJUL74.js +142 -0
- package/lib/browser/chunk-6MBJUL74.js.map +1 -0
- package/lib/browser/chunk-7A4UJNSW.mjs +196 -0
- package/lib/browser/chunk-7A4UJNSW.mjs.map +1 -0
- package/lib/browser/{chunk-PSX5J3RF.js → chunk-7GVJEW45.js} +30 -81
- package/lib/browser/chunk-7GVJEW45.js.map +1 -0
- package/lib/browser/{chunk-2CRB3JAQ.js → chunk-FXSPMSSQ.js} +1 -1
- package/lib/browser/chunk-FXSPMSSQ.js.map +1 -0
- package/lib/browser/{chunk-OMISYKWR.mjs → chunk-GGUENBDN.mjs} +1 -1
- package/lib/browser/chunk-GGUENBDN.mjs.map +1 -0
- package/lib/browser/chunk-NU2MPFD6.mjs +142 -0
- package/lib/browser/chunk-NU2MPFD6.mjs.map +1 -0
- package/lib/browser/chunk-VRKVKT62.js +196 -0
- package/lib/browser/chunk-VRKVKT62.js.map +1 -0
- package/lib/browser/glossary-7d7adb4b.d.ts +66 -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 +2 -6
- package/lib/browser/interceptors/XMLHttpRequest/index.js +4 -4
- package/lib/browser/interceptors/XMLHttpRequest/index.mjs +3 -3
- package/lib/browser/interceptors/fetch/index.d.ts +1 -1
- package/lib/browser/interceptors/fetch/index.js +4 -4
- package/lib/browser/interceptors/fetch/index.mjs +3 -3
- package/lib/browser/presets/browser.d.ts +1 -1
- package/lib/browser/presets/browser.js +6 -6
- package/lib/browser/presets/browser.mjs +4 -4
- package/lib/node/{BatchInterceptor-2badedde.d.ts → BatchInterceptor-13d40c95.d.ts} +1 -1
- package/lib/node/{Interceptor-88ee47c0.d.ts → Interceptor-a31b1217.d.ts} +35 -13
- package/lib/node/RemoteHttpInterceptor.d.ts +2 -2
- package/lib/node/RemoteHttpInterceptor.js +55 -52
- package/lib/node/RemoteHttpInterceptor.js.map +1 -1
- package/lib/node/RemoteHttpInterceptor.mjs +53 -50
- package/lib/node/RemoteHttpInterceptor.mjs.map +1 -1
- package/lib/node/{chunk-5JMJ55U7.js → chunk-2MWIWEWV.js} +144 -113
- package/lib/node/chunk-2MWIWEWV.js.map +1 -0
- package/lib/node/{chunk-2COJKQQB.js → chunk-42632LKH.js} +3 -3
- package/lib/node/chunk-5WWNCLB3.js +196 -0
- package/lib/node/chunk-5WWNCLB3.js.map +1 -0
- package/lib/node/{chunk-TGTPXCLF.mjs → chunk-BUCULLYM.mjs} +1 -1
- package/lib/node/{chunk-TGTPXCLF.mjs.map → chunk-BUCULLYM.mjs.map} +1 -1
- package/lib/node/{chunk-OJ6O4LSC.mjs → chunk-BZ3Y7YV5.mjs} +1 -1
- package/lib/node/chunk-BZ3Y7YV5.mjs.map +1 -0
- package/lib/node/{chunk-3OJLYEWA.mjs → chunk-CU3YXMM4.mjs} +138 -107
- package/lib/node/chunk-CU3YXMM4.mjs.map +1 -0
- package/lib/node/{chunk-PNWPIDEL.mjs → chunk-HGQLG7KE.mjs} +2 -2
- package/lib/node/{chunk-EIBTX65O.js → chunk-IDEEMJ3F.js} +1 -1
- package/lib/node/chunk-IDEEMJ3F.js.map +1 -0
- package/lib/node/chunk-KY3RJ2M3.mjs +196 -0
- package/lib/node/chunk-KY3RJ2M3.mjs.map +1 -0
- package/lib/node/{chunk-PYD4E2EJ.js → chunk-P6QG76R3.js} +34 -85
- package/lib/node/chunk-P6QG76R3.js.map +1 -0
- package/lib/node/{chunk-DV4PBH4D.mjs → chunk-TOV4TYIX.mjs} +29 -80
- package/lib/node/chunk-TOV4TYIX.mjs.map +1 -0
- package/lib/node/{chunk-BFLYGQ6D.js → chunk-YGM3BCJU.js} +1 -1
- package/lib/node/chunk-YGM3BCJU.js.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 +2 -2
- package/lib/node/interceptors/ClientRequest/index.js +4 -4
- package/lib/node/interceptors/ClientRequest/index.mjs +3 -3
- package/lib/node/interceptors/XMLHttpRequest/index.d.ts +2 -6
- package/lib/node/interceptors/XMLHttpRequest/index.js +5 -5
- package/lib/node/interceptors/XMLHttpRequest/index.mjs +4 -4
- package/lib/node/interceptors/fetch/index.d.ts +1 -1
- package/lib/node/interceptors/fetch/index.js +52 -123
- package/lib/node/interceptors/fetch/index.js.map +1 -1
- package/lib/node/interceptors/fetch/index.mjs +50 -121
- 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 +7 -7
- package/lib/node/presets/node.mjs +5 -5
- package/package.json +2 -2
- package/src/InterceptorError.ts +7 -0
- package/src/RemoteHttpInterceptor.ts +62 -57
- package/src/RequestController.test.ts +49 -0
- package/src/RequestController.ts +81 -0
- package/src/glossary.ts +4 -6
- package/src/interceptors/ClientRequest/MockHttpSocket.ts +22 -16
- package/src/interceptors/ClientRequest/index.test.ts +2 -33
- package/src/interceptors/ClientRequest/index.ts +32 -82
- package/src/interceptors/ClientRequest/utils/recordRawHeaders.ts +170 -0
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts +1 -1
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +27 -108
- package/src/interceptors/XMLHttpRequest/index.ts +0 -6
- package/src/interceptors/fetch/index.ts +52 -169
- package/src/utils/handleRequest.ts +213 -0
- package/src/utils/responseUtils.ts +4 -4
- package/lib/browser/chunk-2CRB3JAQ.js.map +0 -1
- package/lib/browser/chunk-732REFPX.mjs.map +0 -1
- package/lib/browser/chunk-MAEPOYB6.mjs +0 -213
- package/lib/browser/chunk-MAEPOYB6.mjs.map +0 -1
- package/lib/browser/chunk-MQJ3JOOK.js +0 -49
- package/lib/browser/chunk-MQJ3JOOK.js.map +0 -1
- package/lib/browser/chunk-OMISYKWR.mjs.map +0 -1
- package/lib/browser/chunk-OUWBQF3Z.mjs +0 -49
- package/lib/browser/chunk-OUWBQF3Z.mjs.map +0 -1
- package/lib/browser/chunk-PSX5J3RF.js.map +0 -1
- package/lib/browser/chunk-WBHIW62P.js +0 -213
- package/lib/browser/chunk-WBHIW62P.js.map +0 -1
- package/lib/browser/glossary-1c204f45.d.ts +0 -44
- package/lib/node/chunk-3OJLYEWA.mjs.map +0 -1
- package/lib/node/chunk-5JMJ55U7.js.map +0 -1
- package/lib/node/chunk-BFLYGQ6D.js.map +0 -1
- package/lib/node/chunk-DV4PBH4D.mjs.map +0 -1
- package/lib/node/chunk-EIBTX65O.js.map +0 -1
- package/lib/node/chunk-KWV3JXSI.mjs +0 -49
- package/lib/node/chunk-KWV3JXSI.mjs.map +0 -1
- package/lib/node/chunk-OJ6O4LSC.mjs.map +0 -1
- package/lib/node/chunk-PYD4E2EJ.js.map +0 -1
- package/lib/node/chunk-UXCYRE4F.js +0 -49
- package/lib/node/chunk-UXCYRE4F.js.map +0 -1
- package/src/utils/getRawFetchHeaders.test.ts +0 -50
- package/src/utils/getRawFetchHeaders.ts +0 -56
- package/src/utils/toInteractiveRequest.ts +0 -23
- /package/lib/node/{chunk-2COJKQQB.js.map → chunk-42632LKH.js.map} +0 -0
- /package/lib/node/{chunk-PNWPIDEL.mjs.map → chunk-HGQLG7KE.mjs.map} +0 -0
|
@@ -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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
|
171
|
-
|
|
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
|
+
}
|
package/src/glossary.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RequestController } from './RequestController'
|
|
2
2
|
|
|
3
3
|
export const IS_PATCHED_MODULE: unique symbol = Symbol('isPatchedModule')
|
|
4
4
|
|
|
@@ -7,8 +7,9 @@ export type RequestCredentials = 'omit' | 'include' | 'same-origin'
|
|
|
7
7
|
export type HttpRequestEventMap = {
|
|
8
8
|
request: [
|
|
9
9
|
args: {
|
|
10
|
-
request:
|
|
10
|
+
request: Request
|
|
11
11
|
requestId: string
|
|
12
|
+
controller: RequestController
|
|
12
13
|
}
|
|
13
14
|
]
|
|
14
15
|
response: [
|
|
@@ -24,10 +25,7 @@ export type HttpRequestEventMap = {
|
|
|
24
25
|
error: unknown
|
|
25
26
|
request: Request
|
|
26
27
|
requestId: string
|
|
27
|
-
controller:
|
|
28
|
-
respondWith(response: Response): void
|
|
29
|
-
errorWith(error?: Error): void
|
|
30
|
-
}
|
|
28
|
+
controller: RequestController
|
|
31
29
|
}
|
|
32
30
|
]
|
|
33
31
|
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type RequestHeadersCompleteCallback,
|
|
5
5
|
type ResponseHeadersCompleteCallback,
|
|
6
6
|
} from '_http_common'
|
|
7
|
-
import { IncomingMessage, ServerResponse } from 'node:http'
|
|
7
|
+
import { STATUS_CODES, IncomingMessage, ServerResponse } from 'node:http'
|
|
8
8
|
import { Readable } from 'node:stream'
|
|
9
9
|
import { invariant } from 'outvariant'
|
|
10
10
|
import { INTERNAL_REQUEST_ID_HEADER_NAME } from '../../Interceptor'
|
|
@@ -13,12 +13,12 @@ import type { NormalizedSocketWriteArgs } from '../Socket/utils/normalizeSocketW
|
|
|
13
13
|
import { isPropertyAccessible } from '../../utils/isPropertyAccessible'
|
|
14
14
|
import { baseUrlFromConnectionOptions } from '../Socket/utils/baseUrlFromConnectionOptions'
|
|
15
15
|
import { parseRawHeaders } from '../Socket/utils/parseRawHeaders'
|
|
16
|
-
import { getRawFetchHeaders } from '../../utils/getRawFetchHeaders'
|
|
17
16
|
import {
|
|
18
17
|
createServerErrorResponse,
|
|
19
18
|
RESPONSE_STATUS_CODES_WITHOUT_BODY,
|
|
20
19
|
} from '../../utils/responseUtils'
|
|
21
20
|
import { createRequestId } from '../../createRequestId'
|
|
21
|
+
import { getRawFetchHeaders } from './utils/recordRawHeaders'
|
|
22
22
|
|
|
23
23
|
type HttpConnectionOptions = any
|
|
24
24
|
|
|
@@ -185,11 +185,12 @@ export class MockHttpSocket extends MockSocket {
|
|
|
185
185
|
const chunkAfterRequestHeaders = chunkString.slice(
|
|
186
186
|
chunk.indexOf('\r\n\r\n')
|
|
187
187
|
)
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const requestHeadersString = Array.from(requestHeaders.entries())
|
|
188
|
+
const rawRequestHeaders = getRawFetchHeaders(this.request!.headers)
|
|
189
|
+
const requestHeadersString = rawRequestHeaders
|
|
191
190
|
// Skip the internal request ID deduplication header.
|
|
192
|
-
.filter(([name]) =>
|
|
191
|
+
.filter(([name]) => {
|
|
192
|
+
return name.toLowerCase() !== INTERNAL_REQUEST_ID_HEADER_NAME
|
|
193
|
+
})
|
|
193
194
|
.map(([name, value]) => `${name}: ${value}`)
|
|
194
195
|
.join('\r\n')
|
|
195
196
|
|
|
@@ -304,8 +305,6 @@ export class MockHttpSocket extends MockSocket {
|
|
|
304
305
|
read() {},
|
|
305
306
|
})
|
|
306
307
|
)
|
|
307
|
-
serverResponse.statusCode = response.status
|
|
308
|
-
serverResponse.statusMessage = response.statusText
|
|
309
308
|
|
|
310
309
|
/**
|
|
311
310
|
* @note Remove the `Connection` and `Date` response headers
|
|
@@ -319,17 +318,24 @@ export class MockHttpSocket extends MockSocket {
|
|
|
319
318
|
serverResponse.removeHeader('connection')
|
|
320
319
|
serverResponse.removeHeader('date')
|
|
321
320
|
|
|
321
|
+
const rawResponseHeaders = getRawFetchHeaders(response.headers)
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* @note Call `.writeHead` in order to set the raw response headers
|
|
325
|
+
* in the same case as they were provided by the developer. Using
|
|
326
|
+
* `.setHeader()`/`.appendHeader()` normalizes header names.
|
|
327
|
+
*/
|
|
328
|
+
serverResponse.writeHead(
|
|
329
|
+
response.status,
|
|
330
|
+
response.statusText || STATUS_CODES[response.status],
|
|
331
|
+
rawResponseHeaders
|
|
332
|
+
)
|
|
333
|
+
|
|
322
334
|
// If the developer destroy the socket, gracefully destroy the response.
|
|
323
335
|
this.once('error', () => {
|
|
324
336
|
serverResponse.destroy()
|
|
325
337
|
})
|
|
326
338
|
|
|
327
|
-
// Get the raw headers stored behind the symbol to preserve name casing.
|
|
328
|
-
const headers = getRawFetchHeaders(response.headers) || response.headers
|
|
329
|
-
for (const [name, value] of headers) {
|
|
330
|
-
serverResponse.setHeader(name, value)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
339
|
if (response.body) {
|
|
334
340
|
try {
|
|
335
341
|
const reader = response.body.getReader()
|
|
@@ -372,7 +378,7 @@ export class MockHttpSocket extends MockSocket {
|
|
|
372
378
|
/**
|
|
373
379
|
* Close this socket connection with the given error.
|
|
374
380
|
*/
|
|
375
|
-
public errorWith(error
|
|
381
|
+
public errorWith(error?: Error): void {
|
|
376
382
|
this.destroy(error)
|
|
377
383
|
}
|
|
378
384
|
|
|
@@ -535,7 +541,7 @@ export class MockHttpSocket extends MockSocket {
|
|
|
535
541
|
|
|
536
542
|
// Similarly, create a new stream for each response.
|
|
537
543
|
if (canHaveBody) {
|
|
538
|
-
this.responseStream = new Readable()
|
|
544
|
+
this.responseStream = new Readable({ read() {} })
|
|
539
545
|
}
|
|
540
546
|
|
|
541
547
|
const response = new Response(
|
|
@@ -30,43 +30,12 @@ afterAll(async () => {
|
|
|
30
30
|
await httpServer.close()
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
it('forbids calling "respondWith" multiple times for the same request', async () => {
|
|
34
|
-
const requestUrl = httpServer.http.url('/')
|
|
35
|
-
|
|
36
|
-
interceptor.on('request', function firstRequestListener({ request }) {
|
|
37
|
-
request.respondWith(new Response())
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const secondRequestEmitted = new DeferredPromise<void>()
|
|
41
|
-
interceptor.on('request', function secondRequestListener({ request }) {
|
|
42
|
-
expect(() =>
|
|
43
|
-
request.respondWith(new Response(null, { status: 301 }))
|
|
44
|
-
).toThrow(
|
|
45
|
-
`Failed to respond to "GET ${requestUrl}" request: the "request" event has already been responded to.`
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
secondRequestEmitted.resolve()
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
const request = http.get(requestUrl)
|
|
52
|
-
await secondRequestEmitted
|
|
53
|
-
|
|
54
|
-
const responseReceived = new DeferredPromise<http.IncomingMessage>()
|
|
55
|
-
request.on('response', (response) => {
|
|
56
|
-
responseReceived.resolve(response)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
const response = await responseReceived
|
|
60
|
-
expect(response.statusCode).toBe(200)
|
|
61
|
-
expect(response.statusMessage).toBe('OK')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
33
|
it('abort the request if the abort signal is emitted', async () => {
|
|
65
34
|
const requestUrl = httpServer.http.url('/')
|
|
66
35
|
|
|
67
|
-
interceptor.on('request', async function delayedResponse({
|
|
36
|
+
interceptor.on('request', async function delayedResponse({ controller }) {
|
|
68
37
|
await sleep(1_000)
|
|
69
|
-
|
|
38
|
+
controller.respondWith(new Response())
|
|
70
39
|
})
|
|
71
40
|
|
|
72
41
|
const abortController = new AbortController()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
2
|
import https from 'node:https'
|
|
3
|
-
import { until } from '@open-draft/until'
|
|
4
3
|
import { Interceptor } from '../../Interceptor'
|
|
5
4
|
import type { HttpRequestEventMap } from '../../glossary'
|
|
6
5
|
import {
|
|
@@ -9,11 +8,14 @@ import {
|
|
|
9
8
|
MockHttpSocketResponseCallback,
|
|
10
9
|
} from './MockHttpSocket'
|
|
11
10
|
import { MockAgent, MockHttpsAgent } from './agents'
|
|
11
|
+
import { RequestController } from '../../RequestController'
|
|
12
12
|
import { emitAsync } from '../../utils/emitAsync'
|
|
13
|
-
import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
|
|
14
13
|
import { normalizeClientRequestArgs } from './utils/normalizeClientRequestArgs'
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { handleRequest } from '../../utils/handleRequest'
|
|
15
|
+
import {
|
|
16
|
+
recordRawFetchHeaders,
|
|
17
|
+
restoreHeadersPrototype,
|
|
18
|
+
} from './utils/recordRawHeaders'
|
|
17
19
|
|
|
18
20
|
export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
19
21
|
static symbol = Symbol('client-request-interceptor')
|
|
@@ -104,12 +106,19 @@ export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
|
104
106
|
},
|
|
105
107
|
})
|
|
106
108
|
|
|
109
|
+
// Spy on `Header.prototype.set` and `Header.prototype.append` calls
|
|
110
|
+
// and record the raw header names provided. This is to support
|
|
111
|
+
// `IncomingMessage.prototype.rawHeaders`.
|
|
112
|
+
recordRawFetchHeaders()
|
|
113
|
+
|
|
107
114
|
this.subscriptions.push(() => {
|
|
108
115
|
http.get = originalGet
|
|
109
116
|
http.request = originalRequest
|
|
110
117
|
|
|
111
118
|
https.get = originalHttpsGet
|
|
112
119
|
https.request = originalHttpsRequest
|
|
120
|
+
|
|
121
|
+
restoreHeadersPrototype()
|
|
113
122
|
})
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -118,88 +127,29 @@ export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
|
118
127
|
socket,
|
|
119
128
|
}) => {
|
|
120
129
|
const requestId = Reflect.get(request, kRequestId)
|
|
121
|
-
const
|
|
122
|
-
toInteractiveRequest(request)
|
|
123
|
-
|
|
124
|
-
// TODO: Abstract this bit. We are using it everywhere.
|
|
125
|
-
this.emitter.once('request', ({ requestId: pendingRequestId }) => {
|
|
126
|
-
if (pendingRequestId !== requestId) {
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (requestController.responsePromise.state === 'pending') {
|
|
131
|
-
this.logger.info(
|
|
132
|
-
'request has not been handled in listeners, executing fail-safe listener...'
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
requestController.responsePromise.resolve(undefined)
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
const listenerResult = await until(async () => {
|
|
140
|
-
await emitAsync(this.emitter, 'request', {
|
|
141
|
-
requestId,
|
|
142
|
-
request: interactiveRequest,
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
return await requestController.responsePromise
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
if (listenerResult.error) {
|
|
149
|
-
// Treat thrown Responses as mocked responses.
|
|
150
|
-
if (listenerResult.error instanceof Response) {
|
|
151
|
-
socket.respondWith(listenerResult.error)
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Allow mocking Node-like errors.
|
|
156
|
-
if (isNodeLikeError(listenerResult.error)) {
|
|
157
|
-
socket.errorWith(listenerResult.error)
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Emit the "unhandledException" event to allow the client
|
|
162
|
-
// to opt-out from the default handling of exceptions
|
|
163
|
-
// as 500 error responses.
|
|
164
|
-
if (this.emitter.listenerCount('unhandledException') > 0) {
|
|
165
|
-
await emitAsync(this.emitter, 'unhandledException', {
|
|
166
|
-
error: listenerResult.error,
|
|
167
|
-
request,
|
|
168
|
-
requestId,
|
|
169
|
-
controller: {
|
|
170
|
-
respondWith: socket.respondWith.bind(socket),
|
|
171
|
-
errorWith: socket.errorWith.bind(socket),
|
|
172
|
-
},
|
|
173
|
-
})
|
|
130
|
+
const controller = new RequestController(request)
|
|
174
131
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
132
|
+
const isRequestHandled = await handleRequest({
|
|
133
|
+
request,
|
|
134
|
+
requestId,
|
|
135
|
+
controller,
|
|
136
|
+
emitter: this.emitter,
|
|
137
|
+
onResponse: (response) => {
|
|
138
|
+
socket.respondWith(response)
|
|
139
|
+
},
|
|
140
|
+
onRequestError: (response) => {
|
|
141
|
+
socket.respondWith(response)
|
|
142
|
+
},
|
|
143
|
+
onError: (error) => {
|
|
144
|
+
if (error instanceof Error) {
|
|
145
|
+
socket.errorWith(error)
|
|
181
146
|
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Unhandled exceptions in the request listeners are
|
|
185
|
-
// synonymous to unhandled exceptions on the server.
|
|
186
|
-
// Those are represented as 500 error responses.
|
|
187
|
-
socket.respondWith(createServerErrorResponse(listenerResult.error))
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const mockedResponse = listenerResult.data
|
|
147
|
+
},
|
|
148
|
+
})
|
|
192
149
|
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
* @note The `.respondWith()` method will handle "Response.error()".
|
|
196
|
-
* Maybe we should make all interceptors do that?
|
|
197
|
-
*/
|
|
198
|
-
socket.respondWith(mockedResponse)
|
|
199
|
-
return
|
|
150
|
+
if (!isRequestHandled) {
|
|
151
|
+
return socket.passthrough()
|
|
200
152
|
}
|
|
201
|
-
|
|
202
|
-
socket.passthrough()
|
|
203
153
|
}
|
|
204
154
|
|
|
205
155
|
public onResponse: MockHttpSocketResponseCallback = async ({
|