@mswjs/interceptors 0.17.6 → 0.18.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 +49 -47
- package/lib/Interceptor.js +4 -4
- package/lib/Interceptor.js.map +1 -1
- package/lib/RemoteHttpInterceptor.d.ts +15 -0
- package/lib/RemoteHttpInterceptor.js +86 -56
- package/lib/RemoteHttpInterceptor.js.map +1 -1
- package/lib/glossary.d.ts +3 -14
- package/lib/glossary.js.map +1 -1
- package/lib/index.d.ts +0 -2
- package/lib/index.js +0 -2
- package/lib/index.js.map +1 -1
- package/lib/interceptors/ClientRequest/NodeClientRequest.d.ts +13 -5
- package/lib/interceptors/ClientRequest/NodeClientRequest.js +179 -166
- package/lib/interceptors/ClientRequest/NodeClientRequest.js.map +1 -1
- package/lib/interceptors/ClientRequest/http.get.js +9 -5
- package/lib/interceptors/ClientRequest/http.get.js.map +1 -1
- package/lib/interceptors/ClientRequest/http.request.js +10 -6
- package/lib/interceptors/ClientRequest/http.request.js.map +1 -1
- package/lib/interceptors/ClientRequest/index.d.ts +2 -5
- package/lib/interceptors/ClientRequest/index.js +2 -13
- package/lib/interceptors/ClientRequest/index.js.map +1 -1
- package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js +9 -5
- package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js.map +1 -1
- package/lib/interceptors/ClientRequest/utils/createRequest.d.ts +6 -0
- package/lib/interceptors/ClientRequest/utils/createRequest.js +52 -0
- package/lib/interceptors/ClientRequest/utils/createRequest.js.map +1 -0
- package/lib/interceptors/ClientRequest/utils/createResponse.d.ts +8 -0
- package/lib/interceptors/ClientRequest/utils/createResponse.js +24 -0
- package/lib/interceptors/ClientRequest/utils/createResponse.js.map +1 -0
- package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js +1 -1
- package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js.map +1 -1
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.js +8 -8
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.js.map +1 -1
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.d.ts +1 -1
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js +1 -1
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js.map +1 -1
- package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.d.ts +6 -10
- package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js +203 -143
- package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js.map +1 -1
- package/lib/interceptors/XMLHttpRequest/index.d.ts +2 -2
- package/lib/interceptors/XMLHttpRequest/index.js +2 -2
- package/lib/interceptors/XMLHttpRequest/index.js.map +1 -1
- package/lib/interceptors/XMLHttpRequest/utils/concatArrayBuffer.d.ts +4 -0
- package/lib/interceptors/XMLHttpRequest/utils/concatArrayBuffer.js +14 -0
- package/lib/interceptors/XMLHttpRequest/utils/concatArrayBuffer.js.map +1 -0
- package/lib/interceptors/XMLHttpRequest/utils/createResponse.d.ts +2 -0
- package/lib/interceptors/XMLHttpRequest/utils/createResponse.js +14 -0
- package/lib/interceptors/XMLHttpRequest/utils/createResponse.js.map +1 -0
- package/lib/interceptors/fetch/index.js +24 -81
- package/lib/interceptors/fetch/index.js.map +1 -1
- package/lib/utils/AsyncEventEmitter.js +12 -8
- package/lib/utils/AsyncEventEmitter.js.map +1 -1
- package/lib/utils/RequestWithCredentials.d.ts +7 -0
- package/lib/utils/RequestWithCredentials.js +20 -0
- package/lib/utils/RequestWithCredentials.js.map +1 -0
- package/lib/utils/bufferUtils.d.ts +7 -2
- package/lib/utils/bufferUtils.js +10 -6
- package/lib/utils/bufferUtils.js.map +1 -1
- package/lib/utils/parseJson.d.ts +1 -1
- package/lib/utils/toInteractiveRequest.d.ts +7 -0
- package/lib/utils/toInteractiveRequest.js +20 -0
- package/lib/utils/toInteractiveRequest.js.map +1 -0
- package/package.json +3 -2
- package/src/RemoteHttpInterceptor.ts +84 -34
- package/src/glossary.ts +5 -18
- package/src/index.ts +0 -2
- package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +17 -23
- package/src/interceptors/ClientRequest/NodeClientRequest.ts +177 -153
- package/src/interceptors/ClientRequest/index.test.ts +5 -3
- package/src/interceptors/ClientRequest/index.ts +2 -26
- package/src/interceptors/ClientRequest/utils/createRequest.test.ts +61 -0
- package/src/interceptors/ClientRequest/utils/createRequest.ts +32 -0
- package/src/interceptors/ClientRequest/utils/createResponse.test.ts +24 -0
- package/src/interceptors/ClientRequest/utils/createResponse.ts +22 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +1 -1
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +234 -174
- package/src/interceptors/XMLHttpRequest/index.ts +3 -2
- package/src/interceptors/XMLHttpRequest/utils/concatArrayBuffer.ts +12 -0
- package/src/interceptors/XMLHttpRequest/utils/concateArrayBuffer.test.ts +14 -0
- package/src/interceptors/XMLHttpRequest/utils/createResponse.ts +13 -0
- package/src/interceptors/fetch/index.ts +30 -69
- package/src/utils/RequestWithCredentials.ts +21 -0
- package/src/utils/bufferUtils.ts +10 -5
- package/src/utils/parseJson.ts +1 -1
- package/src/utils/toInteractiveRequest.ts +29 -0
- package/lib/InteractiveIsomorphicRequest.d.ts +0 -7
- package/lib/InteractiveIsomorphicRequest.js +0 -37
- package/lib/InteractiveIsomorphicRequest.js.map +0 -1
- package/lib/IsomorphicRequest.d.ts +0 -24
- package/lib/IsomorphicRequest.js +0 -107
- package/lib/IsomorphicRequest.js.map +0 -1
- package/lib/interceptors/ClientRequest/utils/bodyBufferToString.d.ts +0 -2
- package/lib/interceptors/ClientRequest/utils/bodyBufferToString.js +0 -11
- package/lib/interceptors/ClientRequest/utils/bodyBufferToString.js.map +0 -1
- package/lib/interceptors/ClientRequest/utils/concatChunkToBuffer.d.ts +0 -2
- package/lib/interceptors/ClientRequest/utils/concatChunkToBuffer.js +0 -11
- package/lib/interceptors/ClientRequest/utils/concatChunkToBuffer.js.map +0 -1
- package/lib/interceptors/XMLHttpRequest/utils/bufferFrom.d.ts +0 -5
- package/lib/interceptors/XMLHttpRequest/utils/bufferFrom.js +0 -20
- package/lib/interceptors/XMLHttpRequest/utils/bufferFrom.js.map +0 -1
- package/lib/utils/toIsoResponse.d.ts +0 -5
- package/lib/utils/toIsoResponse.js +0 -18
- package/lib/utils/toIsoResponse.js.map +0 -1
- package/src/InteractiveIsomorphicRequest.ts +0 -24
- package/src/IsomorphicRequest.test.ts +0 -106
- package/src/IsomorphicRequest.ts +0 -86
- package/src/interceptors/ClientRequest/utils/bodyBufferToString.test.ts +0 -16
- package/src/interceptors/ClientRequest/utils/bodyBufferToString.ts +0 -7
- package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.test.ts +0 -13
- package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.ts +0 -10
- package/src/interceptors/XMLHttpRequest/utils/bufferFrom.test.ts +0 -11
- package/src/interceptors/XMLHttpRequest/utils/bufferFrom.ts +0 -16
- package/src/utils/toIsoResponse.test.ts +0 -39
- package/src/utils/toIsoResponse.ts +0 -14
package/src/glossary.ts
CHANGED
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { InteractiveIsomorphicRequest } from './InteractiveIsomorphicRequest'
|
|
3
|
-
import type { IsomorphicRequest } from './IsomorphicRequest'
|
|
1
|
+
import type { InteractiveRequest } from './utils/toInteractiveRequest'
|
|
4
2
|
|
|
5
3
|
export const IS_PATCHED_MODULE: unique symbol = Symbol('isPatchedModule')
|
|
6
4
|
|
|
7
5
|
export type RequestCredentials = 'omit' | 'include' | 'same-origin'
|
|
8
6
|
|
|
9
|
-
export interface IsomorphicResponse {
|
|
10
|
-
status: number
|
|
11
|
-
statusText: string
|
|
12
|
-
headers: Headers
|
|
13
|
-
body?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface MockedResponse
|
|
17
|
-
extends Omit<Partial<IsomorphicResponse>, 'headers'> {
|
|
18
|
-
headers?: HeadersObject
|
|
19
|
-
}
|
|
20
|
-
|
|
21
7
|
export type HttpRequestEventMap = {
|
|
22
|
-
request(request:
|
|
8
|
+
request(request: InteractiveRequest, requestId: string): Promise<void> | void
|
|
23
9
|
response(
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
response: Response,
|
|
11
|
+
request: Request,
|
|
12
|
+
requestId: string
|
|
26
13
|
): Promise<void> | void
|
|
27
14
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { debug } from 'debug'
|
|
5
5
|
import * as express from 'express'
|
|
6
6
|
import { HttpServer } from '@open-draft/test-server/http'
|
|
7
|
+
import { Response } from '@remix-run/web-fetch'
|
|
7
8
|
import { NodeClientRequest } from './NodeClientRequest'
|
|
8
9
|
import { getIncomingMessageBody } from './utils/getIncomingMessageBody'
|
|
9
10
|
import { normalizeClientRequestArgs } from './utils/normalizeClientRequestArgs'
|
|
@@ -49,13 +50,14 @@ test('gracefully finishes the request when it has a mocked response', (done) =>
|
|
|
49
50
|
)
|
|
50
51
|
|
|
51
52
|
emitter.on('request', (request) => {
|
|
52
|
-
request.respondWith(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
request.respondWith(
|
|
54
|
+
new Response('mocked-response', {
|
|
55
|
+
status: 301,
|
|
56
|
+
headers: {
|
|
57
|
+
'x-custom-header': 'yes',
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
)
|
|
59
61
|
})
|
|
60
62
|
|
|
61
63
|
request.on('response', async (response) => {
|
|
@@ -88,10 +90,7 @@ test('responds with a mocked response when requesting an existing hostname', (do
|
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
emitter.on('request', (request) => {
|
|
91
|
-
request.respondWith({
|
|
92
|
-
status: 201,
|
|
93
|
-
body: 'mocked-response',
|
|
94
|
-
})
|
|
93
|
+
request.respondWith(new Response('mocked-response', { status: 201 }))
|
|
95
94
|
})
|
|
96
95
|
|
|
97
96
|
request.on('response', async (response) => {
|
|
@@ -183,10 +182,9 @@ test('does not emit ENOTFOUND error connecting to an inactive server given mocke
|
|
|
183
182
|
|
|
184
183
|
emitter.on('request', async (request) => {
|
|
185
184
|
await sleep(250)
|
|
186
|
-
request.respondWith(
|
|
187
|
-
status: 200,
|
|
188
|
-
|
|
189
|
-
})
|
|
185
|
+
request.respondWith(
|
|
186
|
+
new Response(null, { status: 200, statusText: 'Works' })
|
|
187
|
+
)
|
|
190
188
|
})
|
|
191
189
|
|
|
192
190
|
request.on('error', handleError)
|
|
@@ -212,10 +210,9 @@ test('does not emit ECONNREFUSED error connecting to an inactive server given mo
|
|
|
212
210
|
|
|
213
211
|
emitter.on('request', async (request) => {
|
|
214
212
|
await sleep(250)
|
|
215
|
-
request.respondWith(
|
|
216
|
-
status: 200,
|
|
217
|
-
|
|
218
|
-
})
|
|
213
|
+
request.respondWith(
|
|
214
|
+
new Response(null, { status: 200, statusText: 'Works' })
|
|
215
|
+
)
|
|
219
216
|
})
|
|
220
217
|
|
|
221
218
|
request.on('error', handleError)
|
|
@@ -272,10 +269,7 @@ test('does not send request body to the original server given mocked response',
|
|
|
272
269
|
|
|
273
270
|
emitter.on('request', async (request) => {
|
|
274
271
|
await sleep(200)
|
|
275
|
-
request.respondWith({
|
|
276
|
-
status: 301,
|
|
277
|
-
body: 'mock created!',
|
|
278
|
-
})
|
|
272
|
+
request.respondWith(new Response('mock created!', { status: 301 }))
|
|
279
273
|
})
|
|
280
274
|
|
|
281
275
|
request.write('one')
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import type { Debugger } from 'debug'
|
|
2
|
-
import type { RequestOptions } from 'http'
|
|
3
2
|
import { ClientRequest, IncomingMessage } from 'http'
|
|
4
3
|
import { until } from '@open-draft/until'
|
|
5
|
-
import { Headers, objectToHeaders } from 'headers-polyfill'
|
|
6
|
-
import { MockedResponse } from '../../glossary'
|
|
7
4
|
import type { ClientRequestEmitter } from '.'
|
|
8
|
-
import { concatChunkToBuffer } from './utils/concatChunkToBuffer'
|
|
9
5
|
import {
|
|
6
|
+
ClientRequestEndCallback,
|
|
10
7
|
ClientRequestEndChunk,
|
|
11
8
|
normalizeClientRequestEndArgs,
|
|
12
9
|
} from './utils/normalizeClientRequestEndArgs'
|
|
13
10
|
import { NormalizedClientRequestArgs } from './utils/normalizeClientRequestArgs'
|
|
14
|
-
import { toIsoResponse } from '../../utils/toIsoResponse'
|
|
15
|
-
import { getIncomingMessageBody } from './utils/getIncomingMessageBody'
|
|
16
|
-
import { bodyBufferToString } from './utils/bodyBufferToString'
|
|
17
11
|
import {
|
|
18
12
|
ClientRequestWriteArgs,
|
|
19
13
|
normalizeClientRequestWriteArgs,
|
|
20
14
|
} from './utils/normalizeClientRequestWriteArgs'
|
|
21
15
|
import { cloneIncomingMessage } from './utils/cloneIncomingMessage'
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
16
|
+
import { createResponse } from './utils/createResponse'
|
|
17
|
+
import { createRequest } from './utils/createRequest'
|
|
18
|
+
import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
|
|
19
|
+
import { uuidv4 } from '../../utils/uuid'
|
|
25
20
|
|
|
26
21
|
export type Protocol = 'http' | 'https'
|
|
27
22
|
|
|
@@ -42,8 +37,6 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
42
37
|
'EAI_AGAIN',
|
|
43
38
|
]
|
|
44
39
|
|
|
45
|
-
private url: URL
|
|
46
|
-
private options: RequestOptions
|
|
47
40
|
private response: IncomingMessage
|
|
48
41
|
private emitter: ClientRequestEmitter
|
|
49
42
|
private log: Debugger
|
|
@@ -54,7 +47,8 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
54
47
|
private responseSource: 'mock' | 'bypass' = 'mock'
|
|
55
48
|
private capturedError?: NodeJS.ErrnoException
|
|
56
49
|
|
|
57
|
-
public
|
|
50
|
+
public url: URL
|
|
51
|
+
public requestBuffer: Buffer | null
|
|
58
52
|
|
|
59
53
|
constructor(
|
|
60
54
|
[url, requestOptions, callback]: NormalizedClientRequestArgs,
|
|
@@ -73,19 +67,44 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
73
67
|
})
|
|
74
68
|
|
|
75
69
|
this.url = url
|
|
76
|
-
this.options = requestOptions
|
|
77
70
|
this.emitter = options.emitter
|
|
78
71
|
|
|
72
|
+
// Set request buffer to null by default so that GET/HEAD requests
|
|
73
|
+
// without a body wouldn't suddenly get one.
|
|
74
|
+
this.requestBuffer = null
|
|
75
|
+
|
|
79
76
|
// Construct a mocked response message.
|
|
80
77
|
this.response = new IncomingMessage(this.socket!)
|
|
81
78
|
}
|
|
82
79
|
|
|
80
|
+
private writeRequestBodyChunk(
|
|
81
|
+
chunk: string | Buffer | null,
|
|
82
|
+
encoding?: BufferEncoding
|
|
83
|
+
): void {
|
|
84
|
+
if (chunk == null) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.requestBuffer == null) {
|
|
89
|
+
this.requestBuffer = Buffer.from([])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const resolvedChunk = Buffer.isBuffer(chunk)
|
|
93
|
+
? chunk
|
|
94
|
+
: Buffer.from(chunk, encoding)
|
|
95
|
+
|
|
96
|
+
this.requestBuffer = Buffer.concat([this.requestBuffer, resolvedChunk])
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
write(...args: ClientRequestWriteArgs): boolean {
|
|
84
100
|
const [chunk, encoding, callback] = normalizeClientRequestWriteArgs(args)
|
|
85
101
|
this.log('write:', { chunk, encoding, callback })
|
|
86
102
|
this.chunks.push({ chunk, encoding })
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
|
|
104
|
+
// Write each request body chunk to the internal buffer.
|
|
105
|
+
this.writeRequestBodyChunk(chunk, encoding)
|
|
106
|
+
|
|
107
|
+
this.log('chunk successfully stored!', this.requestBuffer?.byteLength)
|
|
89
108
|
|
|
90
109
|
/**
|
|
91
110
|
* Prevent invoking the callback if the written chunk is empty.
|
|
@@ -106,14 +125,25 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
106
125
|
end(...args: any): this {
|
|
107
126
|
this.log('end', args)
|
|
108
127
|
|
|
128
|
+
const requestId = uuidv4()
|
|
129
|
+
|
|
109
130
|
const [chunk, encoding, callback] = normalizeClientRequestEndArgs(...args)
|
|
110
131
|
this.log('normalized arguments:', { chunk, encoding, callback })
|
|
111
132
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
133
|
+
// Write the last request body chunk passed to the "end()" method.
|
|
134
|
+
this.writeRequestBodyChunk(chunk, encoding || undefined)
|
|
135
|
+
|
|
136
|
+
const capturedRequest = createRequest(this)
|
|
137
|
+
const interactiveRequest = toInteractiveRequest(capturedRequest)
|
|
138
|
+
|
|
139
|
+
// Prevent handling this request if it has already been handled
|
|
140
|
+
// in another (parent) interceptor (like XMLHttpRequest -> ClientRequest).
|
|
141
|
+
// That means some interceptor up the chain has concluded that
|
|
142
|
+
// this request must be performed as-is.
|
|
143
|
+
if (this.getHeader('X-Request-Id') != null) {
|
|
144
|
+
this.removeHeader('X-Request-Id')
|
|
145
|
+
return this.passthrough(chunk, encoding, callback)
|
|
146
|
+
}
|
|
117
147
|
|
|
118
148
|
// Notify the interceptor about the request.
|
|
119
149
|
// This will call any "request" listeners the users have.
|
|
@@ -121,23 +151,25 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
121
151
|
'emitting the "request" event for %d listener(s)...',
|
|
122
152
|
this.emitter.listenerCount('request')
|
|
123
153
|
)
|
|
124
|
-
this.emitter.emit('request',
|
|
154
|
+
this.emitter.emit('request', interactiveRequest, requestId)
|
|
125
155
|
|
|
126
156
|
// Execute the resolver Promise like a side-effect.
|
|
127
157
|
// Node.js 16 forces "ClientRequest.end" to be synchronous and return "this".
|
|
128
158
|
until(async () => {
|
|
129
|
-
await this.emitter.untilIdle(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
159
|
+
await this.emitter.untilIdle(
|
|
160
|
+
'request',
|
|
161
|
+
({ args: [, pendingRequestId] }) => {
|
|
162
|
+
/**
|
|
163
|
+
* @note Await only those listeners that are relevant to this request.
|
|
164
|
+
* This prevents extraneous parallel request from blocking the resolution
|
|
165
|
+
* of another, unrelated request. For example, during response patching,
|
|
166
|
+
* when request resolution is nested.
|
|
167
|
+
*/
|
|
168
|
+
return pendingRequestId === requestId
|
|
169
|
+
}
|
|
170
|
+
)
|
|
138
171
|
|
|
139
|
-
const [mockedResponse] =
|
|
140
|
-
await interactiveIsomorphicRequest.respondWith.invoked()
|
|
172
|
+
const [mockedResponse] = await interactiveRequest.respondWith.invoked()
|
|
141
173
|
this.log('event.respondWith called with:', mockedResponse)
|
|
142
174
|
|
|
143
175
|
return mockedResponse
|
|
@@ -156,96 +188,54 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
156
188
|
return this
|
|
157
189
|
}
|
|
158
190
|
|
|
191
|
+
// Forward any request headers that the "request" listener
|
|
192
|
+
// may have modified before proceeding with this request.
|
|
193
|
+
for (const [headerName, headerValue] of capturedRequest.headers) {
|
|
194
|
+
this.setHeader(headerName, headerValue)
|
|
195
|
+
}
|
|
196
|
+
|
|
159
197
|
if (mockedResponse) {
|
|
198
|
+
const responseClone = mockedResponse.clone()
|
|
199
|
+
|
|
160
200
|
this.log('received mocked response:', mockedResponse)
|
|
161
201
|
this.responseSource = 'mock'
|
|
162
202
|
|
|
163
|
-
const isomorphicResponse = toIsoResponse(mockedResponse)
|
|
164
203
|
this.respondWith(mockedResponse)
|
|
165
|
-
this.log(
|
|
166
|
-
isomorphicResponse.status,
|
|
167
|
-
isomorphicResponse.statusText,
|
|
168
|
-
isomorphicResponse.body,
|
|
169
|
-
'(MOCKED)'
|
|
170
|
-
)
|
|
204
|
+
this.log(mockedResponse.status, mockedResponse.statusText, '(MOCKED)')
|
|
171
205
|
|
|
172
206
|
callback?.()
|
|
173
207
|
|
|
174
208
|
this.log('emitting the custom "response" event...')
|
|
209
|
+
this.emitter.emit('response', responseClone, capturedRequest, requestId)
|
|
175
210
|
|
|
176
|
-
this.
|
|
211
|
+
this.log('request (mock) is completed')
|
|
177
212
|
|
|
178
213
|
return this
|
|
179
214
|
}
|
|
180
215
|
|
|
181
216
|
this.log('no mocked response received!')
|
|
182
217
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// Propagate previously captured errors.
|
|
188
|
-
// For example, a ECONNREFUSED error when connecting to a non-existing host.
|
|
189
|
-
if (this.capturedError) {
|
|
190
|
-
this.emit('error', this.capturedError)
|
|
191
|
-
return this
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Write the request body chunks in the order of ".write()" calls.
|
|
195
|
-
// Note that no request body has been written prior to this point
|
|
196
|
-
// in order to prevent the Socket to communicate with a potentially
|
|
197
|
-
// existing server.
|
|
198
|
-
this.log('writing request chunks...', this.chunks)
|
|
199
|
-
|
|
200
|
-
for (const { chunk, encoding } of this.chunks) {
|
|
201
|
-
if (encoding) {
|
|
202
|
-
super.write(chunk, encoding)
|
|
203
|
-
} else {
|
|
204
|
-
super.write(chunk)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
this.once('error', (error) => {
|
|
209
|
-
this.log('original request error:', error)
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
this.once('abort', () => {
|
|
213
|
-
this.log('original request aborted!')
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
this.once('response-internal', async (response: IncomingMessage) => {
|
|
217
|
-
const responseBody = await getIncomingMessageBody(response)
|
|
218
|
-
this.log(response.statusCode, response.statusMessage, responseBody)
|
|
219
|
-
this.log('original response headers:', response.headers)
|
|
218
|
+
this.once('response-internal', (message: IncomingMessage) => {
|
|
219
|
+
this.log(message.statusCode, message.statusMessage)
|
|
220
|
+
this.log('original response headers:', message.headers)
|
|
220
221
|
|
|
221
222
|
this.log('emitting the custom "response" event...')
|
|
222
|
-
this.emitter.emit(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
this.emitter.emit(
|
|
224
|
+
'response',
|
|
225
|
+
createResponse(message),
|
|
226
|
+
capturedRequest,
|
|
227
|
+
requestId
|
|
228
|
+
)
|
|
228
229
|
})
|
|
229
230
|
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
return super.end(
|
|
233
|
-
...[
|
|
234
|
-
chunk,
|
|
235
|
-
encoding as any,
|
|
236
|
-
() => {
|
|
237
|
-
this.log('original request end!')
|
|
238
|
-
callback?.()
|
|
239
|
-
},
|
|
240
|
-
].filter(Boolean)
|
|
241
|
-
)
|
|
231
|
+
return this.passthrough(chunk, encoding, callback)
|
|
242
232
|
})
|
|
243
233
|
|
|
244
234
|
return this
|
|
245
235
|
}
|
|
246
236
|
|
|
247
237
|
emit(event: string, ...data: any[]) {
|
|
248
|
-
this.log('
|
|
238
|
+
this.log('emit: %s', event)
|
|
249
239
|
|
|
250
240
|
if (event === 'response') {
|
|
251
241
|
this.log('found "response" event, cloning the response...')
|
|
@@ -299,7 +289,65 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
299
289
|
return super.emit(event, ...data)
|
|
300
290
|
}
|
|
301
291
|
|
|
302
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Performs the intercepted request as-is.
|
|
294
|
+
* Replays the captured request body chunks,
|
|
295
|
+
* still emits the internal events, and wraps
|
|
296
|
+
* up the request with `super.end()`.
|
|
297
|
+
*/
|
|
298
|
+
private passthrough(
|
|
299
|
+
chunk: ClientRequestEndChunk | null,
|
|
300
|
+
encoding?: BufferEncoding | null,
|
|
301
|
+
callback?: ClientRequestEndCallback | null
|
|
302
|
+
): this {
|
|
303
|
+
// Set the response source to "bypass".
|
|
304
|
+
// Any errors emitted past this point are not suppressed.
|
|
305
|
+
this.responseSource = 'bypass'
|
|
306
|
+
|
|
307
|
+
// Propagate previously captured errors.
|
|
308
|
+
// For example, a ECONNREFUSED error when connecting to a non-existing host.
|
|
309
|
+
if (this.capturedError) {
|
|
310
|
+
this.emit('error', this.capturedError)
|
|
311
|
+
return this
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.log('writing request chunks...', this.chunks)
|
|
315
|
+
|
|
316
|
+
// Write the request body chunks in the order of ".write()" calls.
|
|
317
|
+
// Note that no request body has been written prior to this point
|
|
318
|
+
// in order to prevent the Socket to communicate with a potentially
|
|
319
|
+
// existing server.
|
|
320
|
+
for (const { chunk, encoding } of this.chunks) {
|
|
321
|
+
if (encoding) {
|
|
322
|
+
super.write(chunk, encoding)
|
|
323
|
+
} else {
|
|
324
|
+
super.write(chunk)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this.once('error', (error) => {
|
|
329
|
+
this.log('original request error:', error)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
this.once('abort', () => {
|
|
333
|
+
this.log('original request aborted!')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
this.once('response-internal', (message: IncomingMessage) => {
|
|
337
|
+
this.log(message.statusCode, message.statusMessage)
|
|
338
|
+
this.log('original response headers:', message.headers)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
this.log('performing original request...')
|
|
342
|
+
|
|
343
|
+
// This call signature is way too dynamic.
|
|
344
|
+
return super.end(...[chunk, encoding as any, callback].filter(Boolean))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Responds to this request instance using a mocked response.
|
|
349
|
+
*/
|
|
350
|
+
private respondWith(mockedResponse: Response): void {
|
|
303
351
|
this.log('responding with a mocked response...', mockedResponse)
|
|
304
352
|
|
|
305
353
|
const { status, statusText, headers, body } = mockedResponse
|
|
@@ -309,29 +357,48 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
309
357
|
if (headers) {
|
|
310
358
|
this.response.headers = {}
|
|
311
359
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
)
|
|
360
|
+
headers.forEach((headerValue, headerName) => {
|
|
361
|
+
/**
|
|
362
|
+
* @note Make sure that multi-value headers are appended correctly.
|
|
363
|
+
*/
|
|
364
|
+
this.response.rawHeaders.push(headerName, headerValue)
|
|
317
365
|
|
|
318
366
|
const insensitiveHeaderName = headerName.toLowerCase()
|
|
319
367
|
const prevHeaders = this.response.headers[insensitiveHeaderName]
|
|
320
368
|
this.response.headers[insensitiveHeaderName] = prevHeaders
|
|
321
369
|
? Array.prototype.concat([], prevHeaders, headerValue)
|
|
322
370
|
: headerValue
|
|
323
|
-
}
|
|
371
|
+
})
|
|
324
372
|
}
|
|
325
373
|
this.log('mocked response headers ready:', headers)
|
|
326
374
|
|
|
327
|
-
|
|
328
|
-
|
|
375
|
+
const closeResponseStream = () => {
|
|
376
|
+
// Push "null" to indicate that the response body is complete
|
|
377
|
+
// and shouldn't be written to anymore.
|
|
378
|
+
this.response.push(null)
|
|
379
|
+
this.response.complete = true
|
|
329
380
|
}
|
|
330
381
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
382
|
+
if (body) {
|
|
383
|
+
const bodyReader = body.getReader()
|
|
384
|
+
const readNextChunk = async (): Promise<void> => {
|
|
385
|
+
const { done, value } = await bodyReader.read()
|
|
386
|
+
|
|
387
|
+
if (done) {
|
|
388
|
+
closeResponseStream()
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// this.response.push(Buffer.from(body))
|
|
393
|
+
this.response.push(value)
|
|
394
|
+
|
|
395
|
+
return readNextChunk()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
readNextChunk()
|
|
399
|
+
} else {
|
|
400
|
+
closeResponseStream()
|
|
401
|
+
}
|
|
335
402
|
|
|
336
403
|
/**
|
|
337
404
|
* Set the internal "res" property to the mocked "OutgoingMessage"
|
|
@@ -360,47 +427,4 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
360
427
|
// @ts-ignore
|
|
361
428
|
this.agent.destroy()
|
|
362
429
|
}
|
|
363
|
-
|
|
364
|
-
private getRequestBody(chunk: ClientRequestEndChunk | null): ArrayBuffer {
|
|
365
|
-
const writtenRequestBody = bodyBufferToString(
|
|
366
|
-
Buffer.concat(this.requestBody)
|
|
367
|
-
)
|
|
368
|
-
this.log('written request body:', writtenRequestBody)
|
|
369
|
-
|
|
370
|
-
// Write the last request body chunk to the internal request body buffer.
|
|
371
|
-
if (chunk) {
|
|
372
|
-
this.requestBody = concatChunkToBuffer(chunk, this.requestBody)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const resolvedRequestBody = Buffer.concat(this.requestBody)
|
|
376
|
-
this.log('resolved request body:', resolvedRequestBody)
|
|
377
|
-
|
|
378
|
-
return getArrayBuffer(resolvedRequestBody)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
private toIsomorphicRequest(body: ArrayBuffer): IsomorphicRequest {
|
|
382
|
-
this.log('creating isomorphic request object...')
|
|
383
|
-
|
|
384
|
-
const outgoingHeaders = this.getHeaders()
|
|
385
|
-
this.log('request outgoing headers:', outgoingHeaders)
|
|
386
|
-
|
|
387
|
-
const headers = new Headers()
|
|
388
|
-
for (const [headerName, headerValue] of Object.entries(outgoingHeaders)) {
|
|
389
|
-
if (!headerValue) {
|
|
390
|
-
continue
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
headers.set(headerName.toLowerCase(), headerValue.toString())
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const isomorphicRequest = new IsomorphicRequest(this.url, {
|
|
397
|
-
body,
|
|
398
|
-
method: this.options.method || 'GET',
|
|
399
|
-
credentials: 'same-origin',
|
|
400
|
-
headers,
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
this.log('successfully created isomorphic request!', isomorphicRequest)
|
|
404
|
-
return isomorphicRequest
|
|
405
|
-
}
|
|
406
430
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as http from 'http'
|
|
2
2
|
import { HttpServer } from '@open-draft/test-server/http'
|
|
3
|
+
import { Response } from '@remix-run/web-fetch'
|
|
3
4
|
import { ClientRequestInterceptor } from '.'
|
|
4
5
|
|
|
5
6
|
const httpServer = new HttpServer((app) => {
|
|
@@ -15,7 +16,6 @@ const interceptor = new ClientRequestInterceptor()
|
|
|
15
16
|
|
|
16
17
|
beforeAll(async () => {
|
|
17
18
|
await httpServer.listen()
|
|
18
|
-
|
|
19
19
|
interceptor.apply()
|
|
20
20
|
})
|
|
21
21
|
|
|
@@ -28,11 +28,13 @@ it('forbids calling "respondWith" multiple times for the same request', (done) =
|
|
|
28
28
|
const requestUrl = httpServer.http.url('/')
|
|
29
29
|
|
|
30
30
|
interceptor.on('request', (request) => {
|
|
31
|
-
request.respondWith(
|
|
31
|
+
request.respondWith(new Response())
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
interceptor.on('request', (request) => {
|
|
35
|
-
expect(() =>
|
|
35
|
+
expect(() =>
|
|
36
|
+
request.respondWith(new Response(null, { status: 301 }))
|
|
37
|
+
).toThrow(
|
|
36
38
|
`Failed to respond to "GET ${requestUrl}" request: the "request" event has already been responded to.`
|
|
37
39
|
)
|
|
38
40
|
|
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import http from 'http'
|
|
2
2
|
import https from 'https'
|
|
3
|
-
import {
|
|
4
|
-
import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
|
|
3
|
+
import { HttpRequestEventMap } from '../../glossary'
|
|
5
4
|
import { Interceptor } from '../../Interceptor'
|
|
6
5
|
import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
|
|
7
6
|
import { get } from './http.get'
|
|
8
7
|
import { request } from './http.request'
|
|
9
8
|
import { NodeClientOptions, Protocol } from './NodeClientRequest'
|
|
10
9
|
|
|
11
|
-
export type MaybePatchedModule<Module> = Module & {
|
|
12
|
-
[IS_PATCHED_MODULE]?: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
10
|
export type ClientRequestEmitter = AsyncEventEmitter<HttpRequestEventMap>
|
|
16
11
|
|
|
17
|
-
export type ClientRequestModules = Map<
|
|
18
|
-
Protocol,
|
|
19
|
-
MaybePatchedModule<typeof http> | MaybePatchedModule<typeof https>
|
|
20
|
-
>
|
|
12
|
+
export type ClientRequestModules = Map<Protocol, typeof http | typeof https>
|
|
21
13
|
|
|
22
14
|
/**
|
|
23
15
|
* Intercept requests made via the `ClientRequest` class.
|
|
@@ -41,17 +33,7 @@ export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
|
41
33
|
for (const [protocol, requestModule] of this.modules) {
|
|
42
34
|
const { request: pureRequest, get: pureGet } = requestModule
|
|
43
35
|
|
|
44
|
-
invariant(
|
|
45
|
-
!requestModule[IS_PATCHED_MODULE],
|
|
46
|
-
'Failed to patch the "%s" module: already patched.',
|
|
47
|
-
protocol
|
|
48
|
-
)
|
|
49
|
-
|
|
50
36
|
this.subscriptions.push(() => {
|
|
51
|
-
Object.defineProperty(requestModule, IS_PATCHED_MODULE, {
|
|
52
|
-
value: undefined,
|
|
53
|
-
})
|
|
54
|
-
|
|
55
37
|
requestModule.request = pureRequest
|
|
56
38
|
requestModule.get = pureGet
|
|
57
39
|
|
|
@@ -73,12 +55,6 @@ export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
|
73
55
|
// Force a line break.
|
|
74
56
|
get(protocol, options)
|
|
75
57
|
|
|
76
|
-
Object.defineProperty(requestModule, IS_PATCHED_MODULE, {
|
|
77
|
-
configurable: true,
|
|
78
|
-
enumerable: true,
|
|
79
|
-
value: true,
|
|
80
|
-
})
|
|
81
|
-
|
|
82
58
|
log('native "%s" module patched!', protocol)
|
|
83
59
|
}
|
|
84
60
|
}
|