@mswjs/interceptors 0.31.1 → 0.32.1
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 +56 -39
- package/lib/node/RemoteHttpInterceptor.d.ts +1 -2
- package/lib/node/RemoteHttpInterceptor.js +11 -11
- package/lib/node/RemoteHttpInterceptor.mjs +5 -5
- package/lib/node/{chunk-LTEXDYJ6.js → chunk-2COJKQQB.js} +3 -3
- package/lib/node/chunk-3OJLYEWA.mjs +963 -0
- package/lib/node/chunk-3OJLYEWA.mjs.map +1 -0
- package/lib/node/chunk-5JMJ55U7.js +963 -0
- package/lib/node/chunk-5JMJ55U7.js.map +1 -0
- package/lib/node/{chunk-E4AC7YAC.js → chunk-BFLYGQ6D.js} +4 -2
- package/lib/node/{chunk-KSHIDGUL.mjs → chunk-DV4PBH4D.mjs} +3 -3
- package/lib/node/{chunk-OUWBQF3Z.mjs → chunk-KWV3JXSI.mjs} +14 -14
- package/lib/node/chunk-KWV3JXSI.mjs.map +1 -0
- package/lib/node/{chunk-6FRASLM3.mjs → chunk-PNWPIDEL.mjs} +2 -2
- package/lib/node/{chunk-APT7KA3B.js → chunk-PYD4E2EJ.js} +13 -13
- package/lib/node/{chunk-Q7POAM5N.mjs → chunk-TGTPXCLF.mjs} +3 -1
- package/lib/node/{chunk-MQJ3JOOK.js → chunk-UXCYRE4F.js} +14 -14
- package/lib/node/chunk-UXCYRE4F.js.map +1 -0
- package/lib/node/index.js +3 -3
- package/lib/node/index.mjs +2 -2
- package/lib/node/interceptors/ClientRequest/index.d.ts +83 -14
- package/lib/node/interceptors/ClientRequest/index.js +4 -4
- package/lib/node/interceptors/ClientRequest/index.mjs +3 -3
- package/lib/node/interceptors/XMLHttpRequest/index.js +4 -4
- package/lib/node/interceptors/XMLHttpRequest/index.mjs +3 -3
- package/lib/node/interceptors/fetch/index.js +10 -10
- package/lib/node/interceptors/fetch/index.mjs +2 -2
- package/lib/node/presets/node.d.ts +2 -3
- package/lib/node/presets/node.js +6 -6
- package/lib/node/presets/node.mjs +4 -4
- package/package.json +2 -2
- package/src/interceptors/ClientRequest/MockHttpSocket.ts +595 -0
- package/src/interceptors/ClientRequest/agents.ts +78 -0
- package/src/interceptors/ClientRequest/index.test.ts +14 -12
- package/src/interceptors/ClientRequest/index.ts +200 -41
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +78 -98
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +40 -22
- package/src/interceptors/Socket/MockSocket.test.ts +264 -0
- package/src/interceptors/Socket/MockSocket.ts +59 -0
- package/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts +26 -0
- package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts +52 -0
- package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts +33 -0
- package/src/interceptors/Socket/utils/parseRawHeaders.ts +10 -0
- package/lib/node/chunk-IS3CIGXU.js +0 -909
- package/lib/node/chunk-IS3CIGXU.js.map +0 -1
- package/lib/node/chunk-MQJ3JOOK.js.map +0 -1
- package/lib/node/chunk-OMOWHUE6.mjs +0 -909
- package/lib/node/chunk-OMOWHUE6.mjs.map +0 -1
- package/lib/node/chunk-OUWBQF3Z.mjs.map +0 -1
- package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +0 -206
- package/src/interceptors/ClientRequest/NodeClientRequest.ts +0 -680
- package/src/interceptors/ClientRequest/http.get.ts +0 -30
- package/src/interceptors/ClientRequest/http.request.ts +0 -27
- package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts +0 -26
- package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts +0 -74
- package/src/interceptors/ClientRequest/utils/createRequest.test.ts +0 -144
- package/src/interceptors/ClientRequest/utils/createRequest.ts +0 -51
- package/src/interceptors/ClientRequest/utils/createResponse.test.ts +0 -53
- package/src/interceptors/ClientRequest/utils/createResponse.ts +0 -55
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts +0 -41
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +0 -53
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts +0 -36
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts +0 -39
- /package/lib/node/{chunk-LTEXDYJ6.js.map → chunk-2COJKQQB.js.map} +0 -0
- /package/lib/node/{chunk-E4AC7YAC.js.map → chunk-BFLYGQ6D.js.map} +0 -0
- /package/lib/node/{chunk-KSHIDGUL.mjs.map → chunk-DV4PBH4D.mjs.map} +0 -0
- /package/lib/node/{chunk-6FRASLM3.mjs.map → chunk-PNWPIDEL.mjs.map} +0 -0
- /package/lib/node/{chunk-APT7KA3B.js.map → chunk-PYD4E2EJ.js.map} +0 -0
- /package/lib/node/{chunk-Q7POAM5N.mjs.map → chunk-TGTPXCLF.mjs.map} +0 -0
|
@@ -1,680 +0,0 @@
|
|
|
1
|
-
import { ClientRequest, IncomingMessage, STATUS_CODES } from 'node:http'
|
|
2
|
-
import type { Logger } from '@open-draft/logger'
|
|
3
|
-
import { until } from '@open-draft/until'
|
|
4
|
-
import { DeferredPromise } from '@open-draft/deferred-promise'
|
|
5
|
-
import type { ClientRequestEmitter } from '.'
|
|
6
|
-
import {
|
|
7
|
-
ClientRequestEndCallback,
|
|
8
|
-
ClientRequestEndChunk,
|
|
9
|
-
normalizeClientRequestEndArgs,
|
|
10
|
-
} from './utils/normalizeClientRequestEndArgs'
|
|
11
|
-
import { NormalizedClientRequestArgs } from './utils/normalizeClientRequestArgs'
|
|
12
|
-
import {
|
|
13
|
-
ClientRequestWriteArgs,
|
|
14
|
-
normalizeClientRequestWriteArgs,
|
|
15
|
-
} from './utils/normalizeClientRequestWriteArgs'
|
|
16
|
-
import { cloneIncomingMessage } from './utils/cloneIncomingMessage'
|
|
17
|
-
import { createResponse } from './utils/createResponse'
|
|
18
|
-
import { createRequest } from './utils/createRequest'
|
|
19
|
-
import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
|
|
20
|
-
import { emitAsync } from '../../utils/emitAsync'
|
|
21
|
-
import { getRawFetchHeaders } from '../../utils/getRawFetchHeaders'
|
|
22
|
-
import { isNodeLikeError } from '../../utils/isNodeLikeError'
|
|
23
|
-
import { INTERNAL_REQUEST_ID_HEADER_NAME } from '../../Interceptor'
|
|
24
|
-
import { createRequestId } from '../../createRequestId'
|
|
25
|
-
import {
|
|
26
|
-
createServerErrorResponse,
|
|
27
|
-
isResponseError,
|
|
28
|
-
} from '../../utils/responseUtils'
|
|
29
|
-
|
|
30
|
-
export type Protocol = 'http' | 'https'
|
|
31
|
-
|
|
32
|
-
enum HttpClientInternalState {
|
|
33
|
-
// Have the concept of an idle request because different
|
|
34
|
-
// request methods can kick off request sending
|
|
35
|
-
// (e.g. ".end()" or ".flushHeaders()").
|
|
36
|
-
Idle,
|
|
37
|
-
Sending,
|
|
38
|
-
Sent,
|
|
39
|
-
MockLookupStart,
|
|
40
|
-
MockLookupEnd,
|
|
41
|
-
ResponseReceived,
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface NodeClientOptions {
|
|
45
|
-
emitter: ClientRequestEmitter
|
|
46
|
-
logger: Logger
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export class NodeClientRequest extends ClientRequest {
|
|
50
|
-
/**
|
|
51
|
-
* The list of internal Node.js errors to suppress while
|
|
52
|
-
* using the "mock" response source.
|
|
53
|
-
*/
|
|
54
|
-
static suppressErrorCodes = [
|
|
55
|
-
'ENOTFOUND',
|
|
56
|
-
'ECONNREFUSED',
|
|
57
|
-
'ECONNRESET',
|
|
58
|
-
'EAI_AGAIN',
|
|
59
|
-
'ENETUNREACH',
|
|
60
|
-
'EHOSTUNREACH',
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Internal state of the request.
|
|
65
|
-
*/
|
|
66
|
-
private state: HttpClientInternalState
|
|
67
|
-
private responseType?: 'mock' | 'passthrough'
|
|
68
|
-
private response: IncomingMessage
|
|
69
|
-
private emitter: ClientRequestEmitter
|
|
70
|
-
private logger: Logger
|
|
71
|
-
private chunks: Array<{
|
|
72
|
-
chunk?: string | Buffer
|
|
73
|
-
encoding?: BufferEncoding
|
|
74
|
-
}> = []
|
|
75
|
-
private capturedError?: NodeJS.ErrnoException
|
|
76
|
-
|
|
77
|
-
public url: URL
|
|
78
|
-
public requestBuffer: Buffer | null
|
|
79
|
-
|
|
80
|
-
constructor(
|
|
81
|
-
[url, requestOptions, callback]: NormalizedClientRequestArgs,
|
|
82
|
-
options: NodeClientOptions
|
|
83
|
-
) {
|
|
84
|
-
super(requestOptions, callback)
|
|
85
|
-
|
|
86
|
-
this.logger = options.logger.extend(
|
|
87
|
-
`request ${requestOptions.method} ${url.href}`
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
this.logger.info('constructing ClientRequest using options:', {
|
|
91
|
-
url,
|
|
92
|
-
requestOptions,
|
|
93
|
-
callback,
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
this.state = HttpClientInternalState.Idle
|
|
97
|
-
this.url = url
|
|
98
|
-
this.emitter = options.emitter
|
|
99
|
-
|
|
100
|
-
// Set request buffer to null by default so that GET/HEAD requests
|
|
101
|
-
// without a body wouldn't suddenly get one.
|
|
102
|
-
this.requestBuffer = null
|
|
103
|
-
|
|
104
|
-
// Construct a mocked response message.
|
|
105
|
-
this.response = new IncomingMessage(this.socket!)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private writeRequestBodyChunk(
|
|
109
|
-
chunk: string | Buffer | null,
|
|
110
|
-
encoding?: BufferEncoding
|
|
111
|
-
): void {
|
|
112
|
-
if (chunk == null) {
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (this.requestBuffer == null) {
|
|
117
|
-
this.requestBuffer = Buffer.from([])
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const resolvedChunk = Buffer.isBuffer(chunk)
|
|
121
|
-
? chunk
|
|
122
|
-
: Buffer.from(chunk, encoding)
|
|
123
|
-
|
|
124
|
-
this.requestBuffer = Buffer.concat([this.requestBuffer, resolvedChunk])
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
write(...args: ClientRequestWriteArgs): boolean {
|
|
128
|
-
const [chunk, encoding, callback] = normalizeClientRequestWriteArgs(args)
|
|
129
|
-
this.logger.info('write:', { chunk, encoding, callback })
|
|
130
|
-
this.chunks.push({ chunk, encoding })
|
|
131
|
-
|
|
132
|
-
// Write each request body chunk to the internal buffer.
|
|
133
|
-
this.writeRequestBodyChunk(chunk, encoding)
|
|
134
|
-
|
|
135
|
-
this.logger.info(
|
|
136
|
-
'chunk successfully stored!',
|
|
137
|
-
this.requestBuffer?.byteLength
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Prevent invoking the callback if the written chunk is empty.
|
|
142
|
-
* @see https://nodejs.org/api/http.html#requestwritechunk-encoding-callback
|
|
143
|
-
*/
|
|
144
|
-
if (!chunk || chunk.length === 0) {
|
|
145
|
-
this.logger.info('written chunk is empty, skipping callback...')
|
|
146
|
-
} else {
|
|
147
|
-
callback?.()
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Do not write the request body chunks to prevent
|
|
151
|
-
// the Socket from sending data to a potentially existing
|
|
152
|
-
// server when there is a mocked response defined.
|
|
153
|
-
return true
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
end(...args: any): this {
|
|
157
|
-
this.logger.info('end', args)
|
|
158
|
-
|
|
159
|
-
const requestId = createRequestId()
|
|
160
|
-
|
|
161
|
-
const [chunk, encoding, callback] = normalizeClientRequestEndArgs(...args)
|
|
162
|
-
this.logger.info('normalized arguments:', { chunk, encoding, callback })
|
|
163
|
-
|
|
164
|
-
// Write the last request body chunk passed to the "end()" method.
|
|
165
|
-
this.writeRequestBodyChunk(chunk, encoding || undefined)
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* @note Mark the request as sent immediately when invoking ".end()".
|
|
169
|
-
* In Node.js, calling ".end()" will flush the remaining request body
|
|
170
|
-
* and mark the request as "finished" immediately ("end" is synchronous)
|
|
171
|
-
* but we delegate that property update to:
|
|
172
|
-
*
|
|
173
|
-
* - respondWith(), in the case of mocked responses;
|
|
174
|
-
* - super.end(), in the case of bypassed responses.
|
|
175
|
-
*
|
|
176
|
-
* For that reason, we have to keep an internal flag for a finished request.
|
|
177
|
-
*/
|
|
178
|
-
this.state = HttpClientInternalState.Sent
|
|
179
|
-
|
|
180
|
-
const capturedRequest = createRequest(this)
|
|
181
|
-
const { interactiveRequest, requestController } =
|
|
182
|
-
toInteractiveRequest(capturedRequest)
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* @todo Remove this modification of the original request
|
|
186
|
-
* and expose the controller alongside it in the "request"
|
|
187
|
-
* listener argument.
|
|
188
|
-
*/
|
|
189
|
-
Object.defineProperty(capturedRequest, 'respondWith', {
|
|
190
|
-
value: requestController.respondWith.bind(requestController),
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
// Prevent handling this request if it has already been handled
|
|
194
|
-
// in another (parent) interceptor (like XMLHttpRequest -> ClientRequest).
|
|
195
|
-
// That means some interceptor up the chain has concluded that
|
|
196
|
-
// this request must be performed as-is.
|
|
197
|
-
if (this.hasHeader(INTERNAL_REQUEST_ID_HEADER_NAME)) {
|
|
198
|
-
this.removeHeader(INTERNAL_REQUEST_ID_HEADER_NAME)
|
|
199
|
-
return this.passthrough(chunk, encoding, callback)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Add the last "request" listener that always resolves
|
|
203
|
-
// the pending response Promise. This way if the consumer
|
|
204
|
-
// hasn't handled the request themselves, we will prevent
|
|
205
|
-
// the response Promise from pending indefinitely.
|
|
206
|
-
this.emitter.once('request', ({ requestId: pendingRequestId }) => {
|
|
207
|
-
/**
|
|
208
|
-
* @note Ignore request events emitted by irrelevant
|
|
209
|
-
* requests. This happens when response patching.
|
|
210
|
-
*/
|
|
211
|
-
if (pendingRequestId !== requestId) {
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (requestController.responsePromise.state === 'pending') {
|
|
216
|
-
this.logger.info(
|
|
217
|
-
'request has not been handled in listeners, executing fail-safe listener...'
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
requestController.responsePromise.resolve(undefined)
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
// Execute the resolver Promise like a side-effect.
|
|
225
|
-
// Node.js 16 forces "ClientRequest.end" to be synchronous and return "this".
|
|
226
|
-
until<unknown, Response | undefined>(async () => {
|
|
227
|
-
// Notify the interceptor about the request.
|
|
228
|
-
// This will call any "request" listeners the users have.
|
|
229
|
-
this.logger.info(
|
|
230
|
-
'emitting the "request" event for %d listener(s)...',
|
|
231
|
-
this.emitter.listenerCount('request')
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
this.state = HttpClientInternalState.MockLookupStart
|
|
235
|
-
|
|
236
|
-
await emitAsync(this.emitter, 'request', {
|
|
237
|
-
request: interactiveRequest,
|
|
238
|
-
requestId,
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
this.logger.info('all "request" listeners done!')
|
|
242
|
-
|
|
243
|
-
const mockedResponse = await requestController.responsePromise
|
|
244
|
-
this.logger.info('event.respondWith called with:', mockedResponse)
|
|
245
|
-
|
|
246
|
-
return mockedResponse
|
|
247
|
-
}).then((resolverResult) => {
|
|
248
|
-
this.logger.info('the listeners promise awaited!')
|
|
249
|
-
|
|
250
|
-
this.state = HttpClientInternalState.MockLookupEnd
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* @fixme We are in the "end()" method that still executes in parallel
|
|
254
|
-
* to our mocking logic here. This can be solved by migrating to the
|
|
255
|
-
* Proxy-based approach and deferring the passthrough "end()" properly.
|
|
256
|
-
* @see https://github.com/mswjs/interceptors/issues/346
|
|
257
|
-
*/
|
|
258
|
-
if (!this.headersSent) {
|
|
259
|
-
// Forward any request headers that the "request" listener
|
|
260
|
-
// may have modified before proceeding with this request.
|
|
261
|
-
for (const [headerName, headerValue] of capturedRequest.headers) {
|
|
262
|
-
this.setHeader(headerName, headerValue)
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (resolverResult.error) {
|
|
267
|
-
this.logger.info(
|
|
268
|
-
'unhandled resolver exception, coercing to an error response...',
|
|
269
|
-
resolverResult.error
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
// Handle thrown Response instances.
|
|
273
|
-
if (resolverResult.error instanceof Response) {
|
|
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
|
-
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Allow throwing Node.js-like errors, like connection rejection errors.
|
|
290
|
-
// Treat them as request errors.
|
|
291
|
-
if (isNodeLikeError(resolverResult.error)) {
|
|
292
|
-
this.errorWith(resolverResult.error)
|
|
293
|
-
return this
|
|
294
|
-
}
|
|
295
|
-
|
|
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
|
-
})
|
|
324
|
-
|
|
325
|
-
return this
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const mockedResponse = resolverResult.data
|
|
329
|
-
|
|
330
|
-
if (mockedResponse) {
|
|
331
|
-
this.logger.info(
|
|
332
|
-
'received mocked response:',
|
|
333
|
-
mockedResponse.status,
|
|
334
|
-
mockedResponse.statusText
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* @note Ignore this request being destroyed by TLS in Node.js
|
|
339
|
-
* due to connection errors.
|
|
340
|
-
*/
|
|
341
|
-
this.destroyed = false
|
|
342
|
-
|
|
343
|
-
// Handle mocked "Response.error" network error responses.
|
|
344
|
-
if (isResponseError(mockedResponse)) {
|
|
345
|
-
this.logger.info(
|
|
346
|
-
'received network error response, erroring request...'
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* There is no standardized error format for network errors
|
|
351
|
-
* in Node.js. Instead, emit a generic TypeError.
|
|
352
|
-
*/
|
|
353
|
-
this.errorWith(new TypeError('Network error'))
|
|
354
|
-
|
|
355
|
-
return this
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const responseClone = mockedResponse.clone()
|
|
359
|
-
|
|
360
|
-
this.respondWith(mockedResponse)
|
|
361
|
-
this.logger.info(
|
|
362
|
-
mockedResponse.status,
|
|
363
|
-
mockedResponse.statusText,
|
|
364
|
-
'(MOCKED)'
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
callback?.()
|
|
368
|
-
|
|
369
|
-
this.logger.info('emitting the custom "response" event...')
|
|
370
|
-
|
|
371
|
-
const responseListenersPromise = emitAsync(this.emitter, 'response', {
|
|
372
|
-
response: responseClone,
|
|
373
|
-
isMockedResponse: true,
|
|
374
|
-
request: capturedRequest,
|
|
375
|
-
requestId,
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
responseListenersPromise.then(() => {
|
|
379
|
-
this.logger.info('request (mock) is completed')
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
// Defer the end of the response until all the response
|
|
383
|
-
// event listeners are done (those can be async).
|
|
384
|
-
this.deferResponseEndUntil(responseListenersPromise, this.response)
|
|
385
|
-
|
|
386
|
-
return this
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
this.logger.info('no mocked response received!')
|
|
390
|
-
|
|
391
|
-
this.once(
|
|
392
|
-
'response-internal',
|
|
393
|
-
(message: IncomingMessage, originalMessage: IncomingMessage) => {
|
|
394
|
-
this.logger.info(message.statusCode, message.statusMessage)
|
|
395
|
-
this.logger.info('original response headers:', message.headers)
|
|
396
|
-
|
|
397
|
-
this.logger.info('emitting the custom "response" event...')
|
|
398
|
-
|
|
399
|
-
const responseListenersPromise = emitAsync(this.emitter, 'response', {
|
|
400
|
-
response: createResponse(message),
|
|
401
|
-
isMockedResponse: false,
|
|
402
|
-
request: capturedRequest,
|
|
403
|
-
requestId,
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
// Defer the end of the response until all the response
|
|
407
|
-
// event listeners are done (those can be async).
|
|
408
|
-
this.deferResponseEndUntil(responseListenersPromise, originalMessage)
|
|
409
|
-
}
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
return this.passthrough(chunk, encoding, callback)
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
return this
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
emit(event: string, ...data: any[]) {
|
|
419
|
-
this.logger.info('emit: %s', event)
|
|
420
|
-
|
|
421
|
-
if (event === 'response') {
|
|
422
|
-
this.logger.info('found "response" event, cloning the response...')
|
|
423
|
-
|
|
424
|
-
try {
|
|
425
|
-
/**
|
|
426
|
-
* Clone the response object when emitting the "response" event.
|
|
427
|
-
* This prevents the response body stream from locking
|
|
428
|
-
* and allows reading it twice:
|
|
429
|
-
* 1. Internal "response" event from the observer.
|
|
430
|
-
* 2. Any external response body listeners.
|
|
431
|
-
* @see https://github.com/mswjs/interceptors/issues/161
|
|
432
|
-
*/
|
|
433
|
-
const response = data[0] as IncomingMessage
|
|
434
|
-
const firstClone = cloneIncomingMessage(response)
|
|
435
|
-
const secondClone = cloneIncomingMessage(response)
|
|
436
|
-
|
|
437
|
-
this.emit('response-internal', secondClone, firstClone)
|
|
438
|
-
|
|
439
|
-
this.logger.info(
|
|
440
|
-
'response successfully cloned, emitting "response" event...'
|
|
441
|
-
)
|
|
442
|
-
return super.emit(event, firstClone, ...data.slice(1))
|
|
443
|
-
} catch (error) {
|
|
444
|
-
this.logger.info('error when cloning response:', error)
|
|
445
|
-
return super.emit(event, ...data)
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (event === 'error') {
|
|
450
|
-
const error = data[0] as NodeJS.ErrnoException
|
|
451
|
-
const errorCode = error.code || ''
|
|
452
|
-
|
|
453
|
-
this.logger.info('error:\n', error)
|
|
454
|
-
|
|
455
|
-
// Suppress only specific Node.js connection errors.
|
|
456
|
-
if (NodeClientRequest.suppressErrorCodes.includes(errorCode)) {
|
|
457
|
-
// Until we aren't sure whether the request will be
|
|
458
|
-
// passthrough, capture the first emitted connection
|
|
459
|
-
// error in case we have to replay it for this request.
|
|
460
|
-
if (this.state < HttpClientInternalState.MockLookupEnd) {
|
|
461
|
-
if (!this.capturedError) {
|
|
462
|
-
this.capturedError = error
|
|
463
|
-
this.logger.info('captured the first error:', this.capturedError)
|
|
464
|
-
}
|
|
465
|
-
return false
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Ignore any connection errors once we know the request
|
|
469
|
-
// has been resolved with a mocked response. Don't capture
|
|
470
|
-
// them as they won't ever be replayed.
|
|
471
|
-
if (
|
|
472
|
-
this.state === HttpClientInternalState.ResponseReceived &&
|
|
473
|
-
this.responseType === 'mock'
|
|
474
|
-
) {
|
|
475
|
-
return false
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return super.emit(event, ...data)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Performs the intercepted request as-is.
|
|
485
|
-
* Replays the captured request body chunks,
|
|
486
|
-
* still emits the internal events, and wraps
|
|
487
|
-
* up the request with `super.end()`.
|
|
488
|
-
*/
|
|
489
|
-
private passthrough(
|
|
490
|
-
chunk: ClientRequestEndChunk | null,
|
|
491
|
-
encoding?: BufferEncoding | null,
|
|
492
|
-
callback?: ClientRequestEndCallback | null
|
|
493
|
-
): this {
|
|
494
|
-
this.state = HttpClientInternalState.ResponseReceived
|
|
495
|
-
this.responseType = 'passthrough'
|
|
496
|
-
|
|
497
|
-
// Propagate previously captured errors.
|
|
498
|
-
// For example, a ECONNREFUSED error when connecting to a non-existing host.
|
|
499
|
-
if (this.capturedError) {
|
|
500
|
-
this.emit('error', this.capturedError)
|
|
501
|
-
return this
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
this.logger.info('writing request chunks...', this.chunks)
|
|
505
|
-
|
|
506
|
-
// Write the request body chunks in the order of ".write()" calls.
|
|
507
|
-
// Note that no request body has been written prior to this point
|
|
508
|
-
// in order to prevent the Socket to communicate with a potentially
|
|
509
|
-
// existing server.
|
|
510
|
-
for (const { chunk, encoding } of this.chunks) {
|
|
511
|
-
if (encoding) {
|
|
512
|
-
super.write(chunk, encoding)
|
|
513
|
-
} else {
|
|
514
|
-
super.write(chunk)
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
this.once('error', (error) => {
|
|
519
|
-
this.logger.info('original request error:', error)
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
this.once('abort', () => {
|
|
523
|
-
this.logger.info('original request aborted!')
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
this.once('response-internal', (message: IncomingMessage) => {
|
|
527
|
-
this.logger.info(message.statusCode, message.statusMessage)
|
|
528
|
-
this.logger.info('original response headers:', message.headers)
|
|
529
|
-
})
|
|
530
|
-
|
|
531
|
-
this.logger.info('performing original request...')
|
|
532
|
-
|
|
533
|
-
// This call signature is way too dynamic.
|
|
534
|
-
return super.end(...[chunk, encoding as any, callback].filter(Boolean))
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Responds to this request instance using a mocked response.
|
|
539
|
-
*/
|
|
540
|
-
private respondWith(mockedResponse: Response): void {
|
|
541
|
-
this.logger.info('responding with a mocked response...', mockedResponse)
|
|
542
|
-
|
|
543
|
-
this.state = HttpClientInternalState.ResponseReceived
|
|
544
|
-
this.responseType = 'mock'
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Mark the request as finished right before streaming back the response.
|
|
548
|
-
* This is not entirely conventional but this will allow the consumer to
|
|
549
|
-
* modify the outoging request in the interceptor.
|
|
550
|
-
*
|
|
551
|
-
* The request is finished when its headers and bodies have been sent.
|
|
552
|
-
* @see https://nodejs.org/api/http.html#event-finish
|
|
553
|
-
*/
|
|
554
|
-
Object.defineProperties(this, {
|
|
555
|
-
writableFinished: { value: true },
|
|
556
|
-
writableEnded: { value: true },
|
|
557
|
-
})
|
|
558
|
-
this.emit('finish')
|
|
559
|
-
|
|
560
|
-
const { status, statusText, headers, body } = mockedResponse
|
|
561
|
-
this.response.statusCode = status
|
|
562
|
-
this.response.statusMessage = statusText || STATUS_CODES[status]
|
|
563
|
-
|
|
564
|
-
// Try extracting the raw headers from the headers instance.
|
|
565
|
-
// If not possible, fallback to the headers instance as-is.
|
|
566
|
-
const rawHeaders = getRawFetchHeaders(headers) || headers
|
|
567
|
-
|
|
568
|
-
if (rawHeaders) {
|
|
569
|
-
this.response.headers = {}
|
|
570
|
-
|
|
571
|
-
rawHeaders.forEach((headerValue, headerName) => {
|
|
572
|
-
/**
|
|
573
|
-
* @note Make sure that multi-value headers are appended correctly.
|
|
574
|
-
*/
|
|
575
|
-
this.response.rawHeaders.push(headerName, headerValue)
|
|
576
|
-
|
|
577
|
-
const insensitiveHeaderName = headerName.toLowerCase()
|
|
578
|
-
const prevHeaders = this.response.headers[insensitiveHeaderName]
|
|
579
|
-
this.response.headers[insensitiveHeaderName] = prevHeaders
|
|
580
|
-
? Array.prototype.concat([], prevHeaders, headerValue)
|
|
581
|
-
: headerValue
|
|
582
|
-
})
|
|
583
|
-
}
|
|
584
|
-
this.logger.info('mocked response headers ready:', headers)
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Set the internal "res" property to the mocked "OutgoingMessage"
|
|
588
|
-
* to make the "ClientRequest" instance think there's data received
|
|
589
|
-
* from the socket.
|
|
590
|
-
* @see https://github.com/nodejs/node/blob/9c405f2591f5833d0247ed0fafdcd68c5b14ce7a/lib/_http_client.js#L501
|
|
591
|
-
*
|
|
592
|
-
* Set the response immediately so the interceptor could stream data
|
|
593
|
-
* chunks to the request client as they come in.
|
|
594
|
-
*/
|
|
595
|
-
// @ts-ignore
|
|
596
|
-
this.res = this.response
|
|
597
|
-
this.emit('response', this.response)
|
|
598
|
-
|
|
599
|
-
const isResponseStreamFinished = new DeferredPromise<void>()
|
|
600
|
-
|
|
601
|
-
const finishResponseStream = () => {
|
|
602
|
-
this.logger.info('finished response stream!')
|
|
603
|
-
|
|
604
|
-
// Push "null" to indicate that the response body is complete
|
|
605
|
-
// and shouldn't be written to anymore.
|
|
606
|
-
this.response.push(null)
|
|
607
|
-
this.response.complete = true
|
|
608
|
-
|
|
609
|
-
isResponseStreamFinished.resolve()
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (body) {
|
|
613
|
-
const bodyReader = body.getReader()
|
|
614
|
-
const readNextChunk = async (): Promise<void> => {
|
|
615
|
-
const { done, value } = await bodyReader.read()
|
|
616
|
-
|
|
617
|
-
if (done) {
|
|
618
|
-
finishResponseStream()
|
|
619
|
-
return
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
this.response.emit('data', value)
|
|
623
|
-
|
|
624
|
-
return readNextChunk()
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
readNextChunk()
|
|
628
|
-
} else {
|
|
629
|
-
finishResponseStream()
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
isResponseStreamFinished.then(() => {
|
|
633
|
-
this.logger.info('finalizing response...')
|
|
634
|
-
this.response.emit('end')
|
|
635
|
-
this.terminate()
|
|
636
|
-
|
|
637
|
-
this.logger.info('request complete!')
|
|
638
|
-
})
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
private errorWith(error: Error): void {
|
|
642
|
-
this.destroyed = true
|
|
643
|
-
this.emit('error', error)
|
|
644
|
-
this.terminate()
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Terminates a pending request.
|
|
649
|
-
*/
|
|
650
|
-
private terminate(): void {
|
|
651
|
-
/**
|
|
652
|
-
* @note Some request clients (e.g. Octokit, or proxy providers like
|
|
653
|
-
* `global-agent`) create a ClientRequest in a way that it has no Agent set,
|
|
654
|
-
* or does not have a destroy method on it. Now, whether that's correct is
|
|
655
|
-
* debatable, but we should still handle this case gracefully.
|
|
656
|
-
* @see https://github.com/mswjs/interceptors/issues/304
|
|
657
|
-
*/
|
|
658
|
-
// @ts-ignore "agent" is a private property.
|
|
659
|
-
this.agent?.destroy?.()
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
private deferResponseEndUntil(
|
|
663
|
-
promise: Promise<unknown>,
|
|
664
|
-
response: IncomingMessage
|
|
665
|
-
): void {
|
|
666
|
-
response.emit = new Proxy(response.emit, {
|
|
667
|
-
apply: (target, thisArg, args) => {
|
|
668
|
-
const [event] = args
|
|
669
|
-
const callEmit = () => Reflect.apply(target, thisArg, args)
|
|
670
|
-
|
|
671
|
-
if (event === 'end') {
|
|
672
|
-
promise.finally(() => callEmit())
|
|
673
|
-
return this.listenerCount('end') > 0
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
return callEmit()
|
|
677
|
-
},
|
|
678
|
-
})
|
|
679
|
-
}
|
|
680
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { ClientRequest } from 'node:http'
|
|
2
|
-
import {
|
|
3
|
-
NodeClientOptions,
|
|
4
|
-
NodeClientRequest,
|
|
5
|
-
Protocol,
|
|
6
|
-
} from './NodeClientRequest'
|
|
7
|
-
import {
|
|
8
|
-
ClientRequestArgs,
|
|
9
|
-
normalizeClientRequestArgs,
|
|
10
|
-
} from './utils/normalizeClientRequestArgs'
|
|
11
|
-
|
|
12
|
-
export function get(protocol: Protocol, options: NodeClientOptions) {
|
|
13
|
-
return function interceptorsHttpGet(
|
|
14
|
-
...args: ClientRequestArgs
|
|
15
|
-
): ClientRequest {
|
|
16
|
-
const clientRequestArgs = normalizeClientRequestArgs(
|
|
17
|
-
`${protocol}:`,
|
|
18
|
-
...args
|
|
19
|
-
)
|
|
20
|
-
const request = new NodeClientRequest(clientRequestArgs, options)
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @note https://nodejs.org/api/http.html#httpgetoptions-callback
|
|
24
|
-
* "http.get" sets the method to "GET" and calls "req.end()" automatically.
|
|
25
|
-
*/
|
|
26
|
-
request.end()
|
|
27
|
-
|
|
28
|
-
return request
|
|
29
|
-
}
|
|
30
|
-
}
|