@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.
Files changed (114) hide show
  1. package/README.md +49 -47
  2. package/lib/Interceptor.js +4 -4
  3. package/lib/Interceptor.js.map +1 -1
  4. package/lib/RemoteHttpInterceptor.d.ts +15 -0
  5. package/lib/RemoteHttpInterceptor.js +86 -56
  6. package/lib/RemoteHttpInterceptor.js.map +1 -1
  7. package/lib/glossary.d.ts +3 -14
  8. package/lib/glossary.js.map +1 -1
  9. package/lib/index.d.ts +0 -2
  10. package/lib/index.js +0 -2
  11. package/lib/index.js.map +1 -1
  12. package/lib/interceptors/ClientRequest/NodeClientRequest.d.ts +13 -5
  13. package/lib/interceptors/ClientRequest/NodeClientRequest.js +179 -166
  14. package/lib/interceptors/ClientRequest/NodeClientRequest.js.map +1 -1
  15. package/lib/interceptors/ClientRequest/http.get.js +9 -5
  16. package/lib/interceptors/ClientRequest/http.get.js.map +1 -1
  17. package/lib/interceptors/ClientRequest/http.request.js +10 -6
  18. package/lib/interceptors/ClientRequest/http.request.js.map +1 -1
  19. package/lib/interceptors/ClientRequest/index.d.ts +2 -5
  20. package/lib/interceptors/ClientRequest/index.js +2 -13
  21. package/lib/interceptors/ClientRequest/index.js.map +1 -1
  22. package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js +9 -5
  23. package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js.map +1 -1
  24. package/lib/interceptors/ClientRequest/utils/createRequest.d.ts +6 -0
  25. package/lib/interceptors/ClientRequest/utils/createRequest.js +52 -0
  26. package/lib/interceptors/ClientRequest/utils/createRequest.js.map +1 -0
  27. package/lib/interceptors/ClientRequest/utils/createResponse.d.ts +8 -0
  28. package/lib/interceptors/ClientRequest/utils/createResponse.js +24 -0
  29. package/lib/interceptors/ClientRequest/utils/createResponse.js.map +1 -0
  30. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js +1 -1
  31. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js.map +1 -1
  32. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.js +8 -8
  33. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.js.map +1 -1
  34. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.d.ts +1 -1
  35. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js +1 -1
  36. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js.map +1 -1
  37. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.d.ts +6 -10
  38. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js +203 -143
  39. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js.map +1 -1
  40. package/lib/interceptors/XMLHttpRequest/index.d.ts +2 -2
  41. package/lib/interceptors/XMLHttpRequest/index.js +2 -2
  42. package/lib/interceptors/XMLHttpRequest/index.js.map +1 -1
  43. package/lib/interceptors/XMLHttpRequest/utils/concatArrayBuffer.d.ts +4 -0
  44. package/lib/interceptors/XMLHttpRequest/utils/concatArrayBuffer.js +14 -0
  45. package/lib/interceptors/XMLHttpRequest/utils/concatArrayBuffer.js.map +1 -0
  46. package/lib/interceptors/XMLHttpRequest/utils/createResponse.d.ts +2 -0
  47. package/lib/interceptors/XMLHttpRequest/utils/createResponse.js +14 -0
  48. package/lib/interceptors/XMLHttpRequest/utils/createResponse.js.map +1 -0
  49. package/lib/interceptors/fetch/index.js +24 -81
  50. package/lib/interceptors/fetch/index.js.map +1 -1
  51. package/lib/utils/AsyncEventEmitter.js +12 -8
  52. package/lib/utils/AsyncEventEmitter.js.map +1 -1
  53. package/lib/utils/RequestWithCredentials.d.ts +7 -0
  54. package/lib/utils/RequestWithCredentials.js +20 -0
  55. package/lib/utils/RequestWithCredentials.js.map +1 -0
  56. package/lib/utils/bufferUtils.d.ts +7 -2
  57. package/lib/utils/bufferUtils.js +10 -6
  58. package/lib/utils/bufferUtils.js.map +1 -1
  59. package/lib/utils/parseJson.d.ts +1 -1
  60. package/lib/utils/toInteractiveRequest.d.ts +7 -0
  61. package/lib/utils/toInteractiveRequest.js +20 -0
  62. package/lib/utils/toInteractiveRequest.js.map +1 -0
  63. package/package.json +3 -2
  64. package/src/RemoteHttpInterceptor.ts +84 -34
  65. package/src/glossary.ts +5 -18
  66. package/src/index.ts +0 -2
  67. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +17 -23
  68. package/src/interceptors/ClientRequest/NodeClientRequest.ts +177 -153
  69. package/src/interceptors/ClientRequest/index.test.ts +5 -3
  70. package/src/interceptors/ClientRequest/index.ts +2 -26
  71. package/src/interceptors/ClientRequest/utils/createRequest.test.ts +61 -0
  72. package/src/interceptors/ClientRequest/utils/createRequest.ts +32 -0
  73. package/src/interceptors/ClientRequest/utils/createResponse.test.ts +24 -0
  74. package/src/interceptors/ClientRequest/utils/createResponse.ts +22 -0
  75. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +1 -1
  76. package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +234 -174
  77. package/src/interceptors/XMLHttpRequest/index.ts +3 -2
  78. package/src/interceptors/XMLHttpRequest/utils/concatArrayBuffer.ts +12 -0
  79. package/src/interceptors/XMLHttpRequest/utils/concateArrayBuffer.test.ts +14 -0
  80. package/src/interceptors/XMLHttpRequest/utils/createResponse.ts +13 -0
  81. package/src/interceptors/fetch/index.ts +30 -69
  82. package/src/utils/RequestWithCredentials.ts +21 -0
  83. package/src/utils/bufferUtils.ts +10 -5
  84. package/src/utils/parseJson.ts +1 -1
  85. package/src/utils/toInteractiveRequest.ts +29 -0
  86. package/lib/InteractiveIsomorphicRequest.d.ts +0 -7
  87. package/lib/InteractiveIsomorphicRequest.js +0 -37
  88. package/lib/InteractiveIsomorphicRequest.js.map +0 -1
  89. package/lib/IsomorphicRequest.d.ts +0 -24
  90. package/lib/IsomorphicRequest.js +0 -107
  91. package/lib/IsomorphicRequest.js.map +0 -1
  92. package/lib/interceptors/ClientRequest/utils/bodyBufferToString.d.ts +0 -2
  93. package/lib/interceptors/ClientRequest/utils/bodyBufferToString.js +0 -11
  94. package/lib/interceptors/ClientRequest/utils/bodyBufferToString.js.map +0 -1
  95. package/lib/interceptors/ClientRequest/utils/concatChunkToBuffer.d.ts +0 -2
  96. package/lib/interceptors/ClientRequest/utils/concatChunkToBuffer.js +0 -11
  97. package/lib/interceptors/ClientRequest/utils/concatChunkToBuffer.js.map +0 -1
  98. package/lib/interceptors/XMLHttpRequest/utils/bufferFrom.d.ts +0 -5
  99. package/lib/interceptors/XMLHttpRequest/utils/bufferFrom.js +0 -20
  100. package/lib/interceptors/XMLHttpRequest/utils/bufferFrom.js.map +0 -1
  101. package/lib/utils/toIsoResponse.d.ts +0 -5
  102. package/lib/utils/toIsoResponse.js +0 -18
  103. package/lib/utils/toIsoResponse.js.map +0 -1
  104. package/src/InteractiveIsomorphicRequest.ts +0 -24
  105. package/src/IsomorphicRequest.test.ts +0 -106
  106. package/src/IsomorphicRequest.ts +0 -86
  107. package/src/interceptors/ClientRequest/utils/bodyBufferToString.test.ts +0 -16
  108. package/src/interceptors/ClientRequest/utils/bodyBufferToString.ts +0 -7
  109. package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.test.ts +0 -13
  110. package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.ts +0 -10
  111. package/src/interceptors/XMLHttpRequest/utils/bufferFrom.test.ts +0 -11
  112. package/src/interceptors/XMLHttpRequest/utils/bufferFrom.ts +0 -16
  113. package/src/utils/toIsoResponse.test.ts +0 -39
  114. package/src/utils/toIsoResponse.ts +0 -14
package/src/glossary.ts CHANGED
@@ -1,27 +1,14 @@
1
- import type { HeadersObject, Headers } from 'headers-polyfill'
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: InteractiveIsomorphicRequest): Promise<void> | void
8
+ request(request: InteractiveRequest, requestId: string): Promise<void> | void
23
9
  response(
24
- request: IsomorphicRequest,
25
- response: IsomorphicResponse
10
+ response: Response,
11
+ request: Request,
12
+ requestId: string
26
13
  ): Promise<void> | void
27
14
  }
package/src/index.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  export * from './glossary'
2
2
  export * from './Interceptor'
3
3
  export * from './BatchInterceptor'
4
- export * from './IsomorphicRequest'
5
- export * from './InteractiveIsomorphicRequest'
6
4
 
7
5
  /* Utils */
8
6
  export { getCleanUrl } from './utils/getCleanUrl'
@@ -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
- status: 301,
54
- headers: {
55
- 'x-custom-header': 'yes',
56
- },
57
- body: 'mocked-response',
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
- statusText: 'Works',
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
- statusText: 'Works',
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 { IsomorphicRequest } from '../../IsomorphicRequest'
23
- import { InteractiveIsomorphicRequest } from '../../InteractiveIsomorphicRequest'
24
- import { getArrayBuffer } from '../../utils/bufferUtils'
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 requestBody: Buffer[] = []
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
- this.requestBody = concatChunkToBuffer(chunk, this.requestBody)
88
- this.log('chunk successfully stored!', this.requestBody)
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
- const requestBody = this.getRequestBody(chunk)
113
- const isomorphicRequest = this.toIsomorphicRequest(requestBody)
114
- const interactiveIsomorphicRequest = new InteractiveIsomorphicRequest(
115
- isomorphicRequest
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', interactiveIsomorphicRequest)
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('request', ({ args: [request] }) => {
130
- /**
131
- * @note Await only those listeners that are relevant to this request.
132
- * This prevents extraneous parallel request from blocking the resolution
133
- * of another, unrelated request. For example, during response patching,
134
- * when request resolution is nested.
135
- */
136
- return request.id === interactiveIsomorphicRequest.id
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.emitter.emit('response', isomorphicRequest, isomorphicResponse)
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
- // Set the response source to "bypass".
184
- // Any errors emitted past this point are not suppressed.
185
- this.responseSource = 'bypass'
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('response', isomorphicRequest, {
223
- status: response.statusCode || 200,
224
- statusText: response.statusMessage || 'OK',
225
- headers: objectToHeaders(response.headers),
226
- body: responseBody,
227
- })
223
+ this.emitter.emit(
224
+ 'response',
225
+ createResponse(message),
226
+ capturedRequest,
227
+ requestId
228
+ )
228
229
  })
229
230
 
230
- this.log('performing original request...')
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('event:%s', event)
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
- private respondWith(mockedResponse: MockedResponse): void {
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
- for (const [headerName, headerValue] of Object.entries(headers)) {
313
- this.response.rawHeaders.push(
314
- headerName,
315
- ...(Array.isArray(headerValue) ? headerValue : [headerValue])
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
- if (body) {
328
- this.response.push(Buffer.from(body))
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
- // Push "null" to indicate that the response body is complete
332
- // and shouldn't be written to anymore.
333
- this.response.push(null)
334
- this.response.complete = true
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({ status: 200 })
31
+ request.respondWith(new Response())
32
32
  })
33
33
 
34
34
  interceptor.on('request', (request) => {
35
- expect(() => request.respondWith({ status: 301 })).toThrow(
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 { invariant } from 'outvariant'
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
  }