@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.
Files changed (69) hide show
  1. package/README.md +56 -39
  2. package/lib/node/RemoteHttpInterceptor.d.ts +1 -2
  3. package/lib/node/RemoteHttpInterceptor.js +11 -11
  4. package/lib/node/RemoteHttpInterceptor.mjs +5 -5
  5. package/lib/node/{chunk-LTEXDYJ6.js → chunk-2COJKQQB.js} +3 -3
  6. package/lib/node/chunk-3OJLYEWA.mjs +963 -0
  7. package/lib/node/chunk-3OJLYEWA.mjs.map +1 -0
  8. package/lib/node/chunk-5JMJ55U7.js +963 -0
  9. package/lib/node/chunk-5JMJ55U7.js.map +1 -0
  10. package/lib/node/{chunk-E4AC7YAC.js → chunk-BFLYGQ6D.js} +4 -2
  11. package/lib/node/{chunk-KSHIDGUL.mjs → chunk-DV4PBH4D.mjs} +3 -3
  12. package/lib/node/{chunk-OUWBQF3Z.mjs → chunk-KWV3JXSI.mjs} +14 -14
  13. package/lib/node/chunk-KWV3JXSI.mjs.map +1 -0
  14. package/lib/node/{chunk-6FRASLM3.mjs → chunk-PNWPIDEL.mjs} +2 -2
  15. package/lib/node/{chunk-APT7KA3B.js → chunk-PYD4E2EJ.js} +13 -13
  16. package/lib/node/{chunk-Q7POAM5N.mjs → chunk-TGTPXCLF.mjs} +3 -1
  17. package/lib/node/{chunk-MQJ3JOOK.js → chunk-UXCYRE4F.js} +14 -14
  18. package/lib/node/chunk-UXCYRE4F.js.map +1 -0
  19. package/lib/node/index.js +3 -3
  20. package/lib/node/index.mjs +2 -2
  21. package/lib/node/interceptors/ClientRequest/index.d.ts +83 -14
  22. package/lib/node/interceptors/ClientRequest/index.js +4 -4
  23. package/lib/node/interceptors/ClientRequest/index.mjs +3 -3
  24. package/lib/node/interceptors/XMLHttpRequest/index.js +4 -4
  25. package/lib/node/interceptors/XMLHttpRequest/index.mjs +3 -3
  26. package/lib/node/interceptors/fetch/index.js +10 -10
  27. package/lib/node/interceptors/fetch/index.mjs +2 -2
  28. package/lib/node/presets/node.d.ts +2 -3
  29. package/lib/node/presets/node.js +6 -6
  30. package/lib/node/presets/node.mjs +4 -4
  31. package/package.json +2 -2
  32. package/src/interceptors/ClientRequest/MockHttpSocket.ts +595 -0
  33. package/src/interceptors/ClientRequest/agents.ts +78 -0
  34. package/src/interceptors/ClientRequest/index.test.ts +14 -12
  35. package/src/interceptors/ClientRequest/index.ts +200 -41
  36. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +78 -98
  37. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +40 -22
  38. package/src/interceptors/Socket/MockSocket.test.ts +264 -0
  39. package/src/interceptors/Socket/MockSocket.ts +59 -0
  40. package/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts +26 -0
  41. package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts +52 -0
  42. package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts +33 -0
  43. package/src/interceptors/Socket/utils/parseRawHeaders.ts +10 -0
  44. package/lib/node/chunk-IS3CIGXU.js +0 -909
  45. package/lib/node/chunk-IS3CIGXU.js.map +0 -1
  46. package/lib/node/chunk-MQJ3JOOK.js.map +0 -1
  47. package/lib/node/chunk-OMOWHUE6.mjs +0 -909
  48. package/lib/node/chunk-OMOWHUE6.mjs.map +0 -1
  49. package/lib/node/chunk-OUWBQF3Z.mjs.map +0 -1
  50. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +0 -206
  51. package/src/interceptors/ClientRequest/NodeClientRequest.ts +0 -680
  52. package/src/interceptors/ClientRequest/http.get.ts +0 -30
  53. package/src/interceptors/ClientRequest/http.request.ts +0 -27
  54. package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts +0 -26
  55. package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts +0 -74
  56. package/src/interceptors/ClientRequest/utils/createRequest.test.ts +0 -144
  57. package/src/interceptors/ClientRequest/utils/createRequest.ts +0 -51
  58. package/src/interceptors/ClientRequest/utils/createResponse.test.ts +0 -53
  59. package/src/interceptors/ClientRequest/utils/createResponse.ts +0 -55
  60. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts +0 -41
  61. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +0 -53
  62. package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts +0 -36
  63. package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts +0 -39
  64. /package/lib/node/{chunk-LTEXDYJ6.js.map → chunk-2COJKQQB.js.map} +0 -0
  65. /package/lib/node/{chunk-E4AC7YAC.js.map → chunk-BFLYGQ6D.js.map} +0 -0
  66. /package/lib/node/{chunk-KSHIDGUL.mjs.map → chunk-DV4PBH4D.mjs.map} +0 -0
  67. /package/lib/node/{chunk-6FRASLM3.mjs.map → chunk-PNWPIDEL.mjs.map} +0 -0
  68. /package/lib/node/{chunk-APT7KA3B.js.map → chunk-PYD4E2EJ.js.map} +0 -0
  69. /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
- }