@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
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import net from 'node:net'
|
|
2
|
+
import {
|
|
3
|
+
HTTPParser,
|
|
4
|
+
type RequestHeadersCompleteCallback,
|
|
5
|
+
type ResponseHeadersCompleteCallback,
|
|
6
|
+
} from '_http_common'
|
|
7
|
+
import { IncomingMessage, ServerResponse } from 'node:http'
|
|
8
|
+
import { Readable } from 'node:stream'
|
|
9
|
+
import { invariant } from 'outvariant'
|
|
10
|
+
import { INTERNAL_REQUEST_ID_HEADER_NAME } from '../../Interceptor'
|
|
11
|
+
import { MockSocket } from '../Socket/MockSocket'
|
|
12
|
+
import type { NormalizedSocketWriteArgs } from '../Socket/utils/normalizeSocketWriteArgs'
|
|
13
|
+
import { isPropertyAccessible } from '../../utils/isPropertyAccessible'
|
|
14
|
+
import { baseUrlFromConnectionOptions } from '../Socket/utils/baseUrlFromConnectionOptions'
|
|
15
|
+
import { parseRawHeaders } from '../Socket/utils/parseRawHeaders'
|
|
16
|
+
import { getRawFetchHeaders } from '../../utils/getRawFetchHeaders'
|
|
17
|
+
import {
|
|
18
|
+
createServerErrorResponse,
|
|
19
|
+
RESPONSE_STATUS_CODES_WITHOUT_BODY,
|
|
20
|
+
} from '../../utils/responseUtils'
|
|
21
|
+
import { createRequestId } from '../../createRequestId'
|
|
22
|
+
|
|
23
|
+
type HttpConnectionOptions = any
|
|
24
|
+
|
|
25
|
+
export type MockHttpSocketRequestCallback = (args: {
|
|
26
|
+
requestId: string
|
|
27
|
+
request: Request
|
|
28
|
+
socket: MockHttpSocket
|
|
29
|
+
}) => void
|
|
30
|
+
|
|
31
|
+
export type MockHttpSocketResponseCallback = (args: {
|
|
32
|
+
requestId: string
|
|
33
|
+
request: Request
|
|
34
|
+
response: Response
|
|
35
|
+
isMockedResponse: boolean
|
|
36
|
+
socket: MockHttpSocket
|
|
37
|
+
}) => Promise<void>
|
|
38
|
+
|
|
39
|
+
interface MockHttpSocketOptions {
|
|
40
|
+
connectionOptions: HttpConnectionOptions
|
|
41
|
+
createConnection: () => net.Socket
|
|
42
|
+
onRequest: MockHttpSocketRequestCallback
|
|
43
|
+
onResponse: MockHttpSocketResponseCallback
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const kRequestId = Symbol('kRequestId')
|
|
47
|
+
|
|
48
|
+
export class MockHttpSocket extends MockSocket {
|
|
49
|
+
private connectionOptions: HttpConnectionOptions
|
|
50
|
+
private createConnection: () => net.Socket
|
|
51
|
+
private baseUrl: URL
|
|
52
|
+
|
|
53
|
+
private onRequest: MockHttpSocketRequestCallback
|
|
54
|
+
private onResponse: MockHttpSocketResponseCallback
|
|
55
|
+
private responseListenersPromise?: Promise<void>
|
|
56
|
+
|
|
57
|
+
private writeBuffer: Array<NormalizedSocketWriteArgs> = []
|
|
58
|
+
private request?: Request
|
|
59
|
+
private requestParser: HTTPParser<0>
|
|
60
|
+
private requestStream?: Readable
|
|
61
|
+
private shouldKeepAlive?: boolean
|
|
62
|
+
|
|
63
|
+
private responseType: 'mock' | 'bypassed' = 'bypassed'
|
|
64
|
+
private responseParser: HTTPParser<1>
|
|
65
|
+
private responseStream?: Readable
|
|
66
|
+
|
|
67
|
+
constructor(options: MockHttpSocketOptions) {
|
|
68
|
+
super({
|
|
69
|
+
write: (chunk, encoding, callback) => {
|
|
70
|
+
this.writeBuffer.push([chunk, encoding, callback])
|
|
71
|
+
|
|
72
|
+
if (chunk) {
|
|
73
|
+
this.requestParser.execute(
|
|
74
|
+
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
read: (chunk) => {
|
|
79
|
+
if (chunk !== null) {
|
|
80
|
+
this.responseParser.execute(
|
|
81
|
+
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
this.connectionOptions = options.connectionOptions
|
|
88
|
+
this.createConnection = options.createConnection
|
|
89
|
+
this.onRequest = options.onRequest
|
|
90
|
+
this.onResponse = options.onResponse
|
|
91
|
+
|
|
92
|
+
this.baseUrl = baseUrlFromConnectionOptions(this.connectionOptions)
|
|
93
|
+
|
|
94
|
+
// Request parser.
|
|
95
|
+
this.requestParser = new HTTPParser()
|
|
96
|
+
this.requestParser.initialize(HTTPParser.REQUEST, {})
|
|
97
|
+
this.requestParser[HTTPParser.kOnHeadersComplete] =
|
|
98
|
+
this.onRequestStart.bind(this)
|
|
99
|
+
this.requestParser[HTTPParser.kOnBody] = this.onRequestBody.bind(this)
|
|
100
|
+
this.requestParser[HTTPParser.kOnMessageComplete] =
|
|
101
|
+
this.onRequestEnd.bind(this)
|
|
102
|
+
|
|
103
|
+
// Response parser.
|
|
104
|
+
this.responseParser = new HTTPParser()
|
|
105
|
+
this.responseParser.initialize(HTTPParser.RESPONSE, {})
|
|
106
|
+
this.responseParser[HTTPParser.kOnHeadersComplete] =
|
|
107
|
+
this.onResponseStart.bind(this)
|
|
108
|
+
this.responseParser[HTTPParser.kOnBody] = this.onResponseBody.bind(this)
|
|
109
|
+
this.responseParser[HTTPParser.kOnMessageComplete] =
|
|
110
|
+
this.onResponseEnd.bind(this)
|
|
111
|
+
|
|
112
|
+
// Once the socket is finished, nothing can write to it
|
|
113
|
+
// anymore. It has also flushed any buffered chunks.
|
|
114
|
+
this.once('finish', () => this.requestParser.free())
|
|
115
|
+
|
|
116
|
+
if (this.baseUrl.protocol === 'https:') {
|
|
117
|
+
Reflect.set(this, 'encrypted', true)
|
|
118
|
+
// The server certificate is not the same as a CA
|
|
119
|
+
// passed to the TLS socket connection options.
|
|
120
|
+
Reflect.set(this, 'authorized', false)
|
|
121
|
+
Reflect.set(this, 'getProtocol', () => 'TLSv1.3')
|
|
122
|
+
Reflect.set(this, 'getSession', () => undefined)
|
|
123
|
+
Reflect.set(this, 'isSessionReused', () => false)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public emit(event: string | symbol, ...args: any[]): boolean {
|
|
128
|
+
const emitEvent = super.emit.bind(this, event as any, ...args)
|
|
129
|
+
|
|
130
|
+
if (this.responseListenersPromise) {
|
|
131
|
+
this.responseListenersPromise.finally(emitEvent)
|
|
132
|
+
return this.listenerCount(event) > 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return emitEvent()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public destroy(error?: Error | undefined): this {
|
|
139
|
+
// Destroy the response parser when the socket gets destroyed.
|
|
140
|
+
// Normally, we shoud listen to the "close" event but it
|
|
141
|
+
// can be suppressed by using the "emitClose: false" option.
|
|
142
|
+
this.responseParser.free()
|
|
143
|
+
|
|
144
|
+
if (error) {
|
|
145
|
+
this.emit('error', error)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return super.destroy(error)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Establish this Socket connection as-is and pipe
|
|
153
|
+
* its data/events through this Socket.
|
|
154
|
+
*/
|
|
155
|
+
public passthrough(): void {
|
|
156
|
+
if (this.destroyed) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const socket = this.createConnection()
|
|
161
|
+
|
|
162
|
+
// If the developer destroys the socket, destroy the original connection.
|
|
163
|
+
this.once('error', (error) => {
|
|
164
|
+
socket.destroy(error)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
this.address = socket.address.bind(socket)
|
|
168
|
+
|
|
169
|
+
// Flush the buffered "socket.write()" calls onto
|
|
170
|
+
// the original socket instance (i.e. write request body).
|
|
171
|
+
// Exhaust the "requestBuffer" in case this Socket
|
|
172
|
+
// gets reused for different requests.
|
|
173
|
+
let writeArgs: NormalizedSocketWriteArgs | undefined
|
|
174
|
+
let headersWritten = false
|
|
175
|
+
|
|
176
|
+
while ((writeArgs = this.writeBuffer.shift())) {
|
|
177
|
+
if (writeArgs !== undefined) {
|
|
178
|
+
if (!headersWritten) {
|
|
179
|
+
const [chunk, encoding, callback] = writeArgs
|
|
180
|
+
const chunkString = chunk.toString()
|
|
181
|
+
const chunkBeforeRequestHeaders = chunkString.slice(
|
|
182
|
+
0,
|
|
183
|
+
chunkString.indexOf('\r\n') + 2
|
|
184
|
+
)
|
|
185
|
+
const chunkAfterRequestHeaders = chunkString.slice(
|
|
186
|
+
chunk.indexOf('\r\n\r\n')
|
|
187
|
+
)
|
|
188
|
+
const requestHeaders =
|
|
189
|
+
getRawFetchHeaders(this.request!.headers) || this.request!.headers
|
|
190
|
+
const requestHeadersString = Array.from(requestHeaders.entries())
|
|
191
|
+
// Skip the internal request ID deduplication header.
|
|
192
|
+
.filter(([name]) => name !== INTERNAL_REQUEST_ID_HEADER_NAME)
|
|
193
|
+
.map(([name, value]) => `${name}: ${value}`)
|
|
194
|
+
.join('\r\n')
|
|
195
|
+
|
|
196
|
+
// Modify the HTTP request message headers
|
|
197
|
+
// to reflect any changes to the request headers
|
|
198
|
+
// from the "request" event listener.
|
|
199
|
+
const headersChunk = `${chunkBeforeRequestHeaders}${requestHeadersString}${chunkAfterRequestHeaders}`
|
|
200
|
+
socket.write(headersChunk, encoding, callback)
|
|
201
|
+
headersWritten = true
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
socket.write(...writeArgs)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Forward TLS Socket properties onto this Socket instance
|
|
210
|
+
// in the case of a TLS/SSL connection.
|
|
211
|
+
if (Reflect.get(socket, 'encrypted')) {
|
|
212
|
+
const tlsProperties = [
|
|
213
|
+
'encrypted',
|
|
214
|
+
'authorized',
|
|
215
|
+
'getProtocol',
|
|
216
|
+
'getSession',
|
|
217
|
+
'isSessionReused',
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
tlsProperties.forEach((propertyName) => {
|
|
221
|
+
Object.defineProperty(this, propertyName, {
|
|
222
|
+
enumerable: true,
|
|
223
|
+
get: () => {
|
|
224
|
+
const value = Reflect.get(socket, propertyName)
|
|
225
|
+
return typeof value === 'function' ? value.bind(socket) : value
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
socket
|
|
232
|
+
.on('lookup', (...args) => this.emit('lookup', ...args))
|
|
233
|
+
.on('connect', () => {
|
|
234
|
+
this.connecting = socket.connecting
|
|
235
|
+
this.emit('connect')
|
|
236
|
+
})
|
|
237
|
+
.on('secureConnect', () => this.emit('secureConnect'))
|
|
238
|
+
.on('secure', () => this.emit('secure'))
|
|
239
|
+
.on('session', (session) => this.emit('session', session))
|
|
240
|
+
.on('ready', () => this.emit('ready'))
|
|
241
|
+
.on('drain', () => this.emit('drain'))
|
|
242
|
+
.on('data', (chunk) => {
|
|
243
|
+
// Push the original response to this socket
|
|
244
|
+
// so it triggers the HTTP response parser. This unifies
|
|
245
|
+
// the handling pipeline for original and mocked response.
|
|
246
|
+
this.push(chunk)
|
|
247
|
+
})
|
|
248
|
+
.on('error', (error) => {
|
|
249
|
+
Reflect.set(this, '_hadError', Reflect.get(socket, '_hadError'))
|
|
250
|
+
this.emit('error', error)
|
|
251
|
+
})
|
|
252
|
+
.on('resume', () => this.emit('resume'))
|
|
253
|
+
.on('timeout', () => this.emit('timeout'))
|
|
254
|
+
.on('prefinish', () => this.emit('prefinish'))
|
|
255
|
+
.on('finish', () => this.emit('finish'))
|
|
256
|
+
.on('close', (hadError) => this.emit('close', hadError))
|
|
257
|
+
.on('end', () => this.emit('end'))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Convert the given Fetch API `Response` instance to an
|
|
262
|
+
* HTTP message and push it to the socket.
|
|
263
|
+
*/
|
|
264
|
+
public async respondWith(response: Response): Promise<void> {
|
|
265
|
+
// Ignore the mocked response if the socket has been destroyed
|
|
266
|
+
// (e.g. aborted or timed out),
|
|
267
|
+
if (this.destroyed) {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Handle "type: error" responses.
|
|
272
|
+
if (isPropertyAccessible(response, 'type') && response.type === 'error') {
|
|
273
|
+
this.errorWith(new TypeError('Network error'))
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// First, emit all the connection events
|
|
278
|
+
// to emulate a successful connection.
|
|
279
|
+
this.mockConnect()
|
|
280
|
+
this.responseType = 'mock'
|
|
281
|
+
|
|
282
|
+
// Flush the write buffer to trigger write callbacks
|
|
283
|
+
// if it hasn't been flushed already (e.g. someone started reading request stream).
|
|
284
|
+
this.flushWriteBuffer()
|
|
285
|
+
|
|
286
|
+
// Create a `ServerResponse` instance to delegate HTTP message parsing,
|
|
287
|
+
// Transfer-Encoding, and other things to Node.js internals.
|
|
288
|
+
const serverResponse = new ServerResponse(new IncomingMessage(this))
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Assign a mock socket instance to the server response to
|
|
292
|
+
* spy on the response chunk writes. Push the transformed response chunks
|
|
293
|
+
* to this `MockHttpSocket` instance to trigger the "data" event.
|
|
294
|
+
* @note Providing the same `MockSocket` instance when creating `ServerResponse`
|
|
295
|
+
* does not have the same effect.
|
|
296
|
+
* @see https://github.com/nodejs/node/blob/10099bb3f7fd97bb9dd9667188426866b3098e07/test/parallel/test-http-server-response-standalone.js#L32
|
|
297
|
+
*/
|
|
298
|
+
serverResponse.assignSocket(
|
|
299
|
+
new MockSocket({
|
|
300
|
+
write: (chunk, encoding, callback) => {
|
|
301
|
+
this.push(chunk, encoding)
|
|
302
|
+
callback?.()
|
|
303
|
+
},
|
|
304
|
+
read() {},
|
|
305
|
+
})
|
|
306
|
+
)
|
|
307
|
+
serverResponse.statusCode = response.status
|
|
308
|
+
serverResponse.statusMessage = response.statusText
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @note Remove the `Connection` and `Date` response headers
|
|
312
|
+
* injected by `ServerResponse` by default. Those are required
|
|
313
|
+
* from the server but the interceptor is NOT technically a server.
|
|
314
|
+
* It's confusing to add response headers that the developer didn't
|
|
315
|
+
* specify themselves. They can always add these if they wish.
|
|
316
|
+
* @see https://www.rfc-editor.org/rfc/rfc9110#field.date
|
|
317
|
+
* @see https://www.rfc-editor.org/rfc/rfc9110#field.connection
|
|
318
|
+
*/
|
|
319
|
+
serverResponse.removeHeader('connection')
|
|
320
|
+
serverResponse.removeHeader('date')
|
|
321
|
+
|
|
322
|
+
// If the developer destroy the socket, gracefully destroy the response.
|
|
323
|
+
this.once('error', () => {
|
|
324
|
+
serverResponse.destroy()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Get the raw headers stored behind the symbol to preserve name casing.
|
|
328
|
+
const headers = getRawFetchHeaders(response.headers) || response.headers
|
|
329
|
+
for (const [name, value] of headers) {
|
|
330
|
+
serverResponse.setHeader(name, value)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (response.body) {
|
|
334
|
+
try {
|
|
335
|
+
const reader = response.body.getReader()
|
|
336
|
+
|
|
337
|
+
while (true) {
|
|
338
|
+
const { done, value } = await reader.read()
|
|
339
|
+
|
|
340
|
+
if (done) {
|
|
341
|
+
serverResponse.end()
|
|
342
|
+
break
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
serverResponse.write(value)
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
// Coerce response stream errors to 500 responses.
|
|
349
|
+
this.respondWith(createServerErrorResponse(error))
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
serverResponse.end()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Close the socket if the connection wasn't marked as keep-alive.
|
|
357
|
+
if (!this.shouldKeepAlive) {
|
|
358
|
+
this.emit('readable')
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @todo @fixme This is likely a hack.
|
|
362
|
+
* Since we push null to the socket, it never propagates to the
|
|
363
|
+
* parser, and the parser never calls "onResponseEnd" to close
|
|
364
|
+
* the response stream. We are closing the stream here manually
|
|
365
|
+
* but that shouldn't be the case.
|
|
366
|
+
*/
|
|
367
|
+
this.responseStream?.push(null)
|
|
368
|
+
this.push(null)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Close this socket connection with the given error.
|
|
374
|
+
*/
|
|
375
|
+
public errorWith(error: Error): void {
|
|
376
|
+
this.destroy(error)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private mockConnect(): void {
|
|
380
|
+
// Calling this method immediately puts the socket
|
|
381
|
+
// into the connected state.
|
|
382
|
+
this.connecting = false
|
|
383
|
+
|
|
384
|
+
const isIPv6 =
|
|
385
|
+
net.isIPv6(this.connectionOptions.hostname) ||
|
|
386
|
+
this.connectionOptions.family === 6
|
|
387
|
+
const addressInfo = {
|
|
388
|
+
address: isIPv6 ? '::1' : '127.0.0.1',
|
|
389
|
+
family: isIPv6 ? 'IPv6' : 'IPv4',
|
|
390
|
+
port: this.connectionOptions.port,
|
|
391
|
+
}
|
|
392
|
+
// Return fake address information for the socket.
|
|
393
|
+
this.address = () => addressInfo
|
|
394
|
+
this.emit(
|
|
395
|
+
'lookup',
|
|
396
|
+
null,
|
|
397
|
+
addressInfo.address,
|
|
398
|
+
addressInfo.family === 'IPv6' ? 6 : 4,
|
|
399
|
+
this.connectionOptions.host
|
|
400
|
+
)
|
|
401
|
+
this.emit('connect')
|
|
402
|
+
this.emit('ready')
|
|
403
|
+
|
|
404
|
+
if (this.baseUrl.protocol === 'https:') {
|
|
405
|
+
this.emit('secure')
|
|
406
|
+
this.emit('secureConnect')
|
|
407
|
+
|
|
408
|
+
// A single TLS connection is represented by two "session" events.
|
|
409
|
+
this.emit(
|
|
410
|
+
'session',
|
|
411
|
+
this.connectionOptions.session ||
|
|
412
|
+
Buffer.from('mock-session-renegotiate')
|
|
413
|
+
)
|
|
414
|
+
this.emit('session', Buffer.from('mock-session-resume'))
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private flushWriteBuffer(): void {
|
|
419
|
+
let args: NormalizedSocketWriteArgs | undefined
|
|
420
|
+
while ((args = this.writeBuffer.shift())) {
|
|
421
|
+
args?.[2]?.()
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private onRequestStart: RequestHeadersCompleteCallback = (
|
|
426
|
+
versionMajor,
|
|
427
|
+
versionMinor,
|
|
428
|
+
rawHeaders,
|
|
429
|
+
_,
|
|
430
|
+
path,
|
|
431
|
+
__,
|
|
432
|
+
___,
|
|
433
|
+
____,
|
|
434
|
+
shouldKeepAlive
|
|
435
|
+
) => {
|
|
436
|
+
this.shouldKeepAlive = shouldKeepAlive
|
|
437
|
+
|
|
438
|
+
const url = new URL(path, this.baseUrl)
|
|
439
|
+
const method = this.connectionOptions.method || 'GET'
|
|
440
|
+
const headers = parseRawHeaders(rawHeaders)
|
|
441
|
+
const canHaveBody = method !== 'GET' && method !== 'HEAD'
|
|
442
|
+
|
|
443
|
+
// Translate the basic authorization in the URL to the request header.
|
|
444
|
+
// Constructing a Request instance with a URL containing auth is no-op.
|
|
445
|
+
if (url.username || url.password) {
|
|
446
|
+
if (!headers.has('authorization')) {
|
|
447
|
+
headers.set('authorization', `Basic ${url.username}:${url.password}`)
|
|
448
|
+
}
|
|
449
|
+
url.username = ''
|
|
450
|
+
url.password = ''
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Create a new stream for each request.
|
|
454
|
+
// If this Socket is reused for multiple requests,
|
|
455
|
+
// this ensures that each request gets its own stream.
|
|
456
|
+
// One Socket instance can only handle one request at a time.
|
|
457
|
+
if (canHaveBody) {
|
|
458
|
+
this.requestStream = new Readable({
|
|
459
|
+
/**
|
|
460
|
+
* @note Provide the `read()` method so a `Readable` could be
|
|
461
|
+
* used as the actual request body (the stream calls "read()").
|
|
462
|
+
* We control the queue in the onRequestBody/End functions.
|
|
463
|
+
*/
|
|
464
|
+
read: () => {
|
|
465
|
+
// If the user attempts to read the request body,
|
|
466
|
+
// flush the write buffer to trigger the callbacks.
|
|
467
|
+
// This way, if the request stream ends in the write callback,
|
|
468
|
+
// it will indeed end correctly.
|
|
469
|
+
this.flushWriteBuffer()
|
|
470
|
+
},
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const requestId = createRequestId()
|
|
475
|
+
this.request = new Request(url, {
|
|
476
|
+
method,
|
|
477
|
+
headers,
|
|
478
|
+
credentials: 'same-origin',
|
|
479
|
+
// @ts-expect-error Undocumented Fetch property.
|
|
480
|
+
duplex: canHaveBody ? 'half' : undefined,
|
|
481
|
+
body: canHaveBody ? (Readable.toWeb(this.requestStream!) as any) : null,
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
Reflect.set(this.request, kRequestId, requestId)
|
|
485
|
+
|
|
486
|
+
// Skip handling the request that's already being handled
|
|
487
|
+
// by another (parent) interceptor. For example, XMLHttpRequest
|
|
488
|
+
// is often implemented via ClientRequest in Node.js (e.g. JSDOM).
|
|
489
|
+
// In that case, XHR interceptor will bubble down to the ClientRequest
|
|
490
|
+
// interceptor. No need to try to handle that request again.
|
|
491
|
+
/**
|
|
492
|
+
* @fixme Stop relying on the "X-Request-Id" request header
|
|
493
|
+
* to figure out if one interceptor has been invoked within another.
|
|
494
|
+
* @see https://github.com/mswjs/interceptors/issues/378
|
|
495
|
+
*/
|
|
496
|
+
if (this.request.headers.has(INTERNAL_REQUEST_ID_HEADER_NAME)) {
|
|
497
|
+
this.passthrough()
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
this.onRequest({
|
|
502
|
+
requestId,
|
|
503
|
+
request: this.request,
|
|
504
|
+
socket: this,
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private onRequestBody(chunk: Buffer): void {
|
|
509
|
+
invariant(
|
|
510
|
+
this.requestStream,
|
|
511
|
+
'Failed to write to a request stream: stream does not exist'
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
this.requestStream.push(chunk)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private onRequestEnd(): void {
|
|
518
|
+
// Request end can be called for requests without body.
|
|
519
|
+
if (this.requestStream) {
|
|
520
|
+
this.requestStream.push(null)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private onResponseStart: ResponseHeadersCompleteCallback = (
|
|
525
|
+
versionMajor,
|
|
526
|
+
versionMinor,
|
|
527
|
+
rawHeaders,
|
|
528
|
+
method,
|
|
529
|
+
url,
|
|
530
|
+
status,
|
|
531
|
+
statusText
|
|
532
|
+
) => {
|
|
533
|
+
const headers = parseRawHeaders(rawHeaders)
|
|
534
|
+
const canHaveBody = !RESPONSE_STATUS_CODES_WITHOUT_BODY.has(status)
|
|
535
|
+
|
|
536
|
+
// Similarly, create a new stream for each response.
|
|
537
|
+
if (canHaveBody) {
|
|
538
|
+
this.responseStream = new Readable()
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const response = new Response(
|
|
542
|
+
/**
|
|
543
|
+
* @note The Fetch API response instance exposed to the consumer
|
|
544
|
+
* is created over the response stream of the HTTP parser. It is NOT
|
|
545
|
+
* related to the Socket instance. This way, you can read response body
|
|
546
|
+
* in response listener while the Socket instance delays the emission
|
|
547
|
+
* of "end" and other events until those response listeners are finished.
|
|
548
|
+
*/
|
|
549
|
+
canHaveBody ? (Readable.toWeb(this.responseStream!) as any) : null,
|
|
550
|
+
{
|
|
551
|
+
status,
|
|
552
|
+
statusText,
|
|
553
|
+
headers,
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
invariant(
|
|
558
|
+
this.request,
|
|
559
|
+
'Failed to handle a response: request does not exist'
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* @fixme Stop relying on the "X-Request-Id" request header
|
|
564
|
+
* to figure out if one interceptor has been invoked within another.
|
|
565
|
+
* @see https://github.com/mswjs/interceptors/issues/378
|
|
566
|
+
*/
|
|
567
|
+
if (this.request.headers.has(INTERNAL_REQUEST_ID_HEADER_NAME)) {
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
this.responseListenersPromise = this.onResponse({
|
|
572
|
+
response,
|
|
573
|
+
isMockedResponse: this.responseType === 'mock',
|
|
574
|
+
requestId: Reflect.get(this.request, kRequestId),
|
|
575
|
+
request: this.request,
|
|
576
|
+
socket: this,
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private onResponseBody(chunk: Buffer) {
|
|
581
|
+
invariant(
|
|
582
|
+
this.responseStream,
|
|
583
|
+
'Failed to write to a response stream: stream does not exist'
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
this.responseStream.push(chunk)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private onResponseEnd(): void {
|
|
590
|
+
// Response end can be called for responses without body.
|
|
591
|
+
if (this.responseStream) {
|
|
592
|
+
this.responseStream.push(null)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import net from 'node:net'
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
import https from 'node:https'
|
|
4
|
+
import {
|
|
5
|
+
MockHttpSocket,
|
|
6
|
+
type MockHttpSocketRequestCallback,
|
|
7
|
+
type MockHttpSocketResponseCallback,
|
|
8
|
+
} from './MockHttpSocket'
|
|
9
|
+
|
|
10
|
+
declare module 'node:http' {
|
|
11
|
+
interface Agent {
|
|
12
|
+
createConnection(options: any, callback: any): net.Socket
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface MockAgentOptions {
|
|
17
|
+
customAgent?: http.RequestOptions['agent']
|
|
18
|
+
onRequest: MockHttpSocketRequestCallback
|
|
19
|
+
onResponse: MockHttpSocketResponseCallback
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class MockAgent extends http.Agent {
|
|
23
|
+
private customAgent?: http.RequestOptions['agent']
|
|
24
|
+
private onRequest: MockHttpSocketRequestCallback
|
|
25
|
+
private onResponse: MockHttpSocketResponseCallback
|
|
26
|
+
|
|
27
|
+
constructor(options: MockAgentOptions) {
|
|
28
|
+
super()
|
|
29
|
+
this.customAgent = options.customAgent
|
|
30
|
+
this.onRequest = options.onRequest
|
|
31
|
+
this.onResponse = options.onResponse
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public createConnection(options: any, callback: any) {
|
|
35
|
+
const createConnection =
|
|
36
|
+
(this.customAgent instanceof http.Agent &&
|
|
37
|
+
this.customAgent.createConnection) ||
|
|
38
|
+
super.createConnection
|
|
39
|
+
|
|
40
|
+
const socket = new MockHttpSocket({
|
|
41
|
+
connectionOptions: options,
|
|
42
|
+
createConnection: createConnection.bind(this, options, callback),
|
|
43
|
+
onRequest: this.onRequest.bind(this),
|
|
44
|
+
onResponse: this.onResponse.bind(this),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return socket
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class MockHttpsAgent extends https.Agent {
|
|
52
|
+
private customAgent?: https.RequestOptions['agent']
|
|
53
|
+
private onRequest: MockHttpSocketRequestCallback
|
|
54
|
+
private onResponse: MockHttpSocketResponseCallback
|
|
55
|
+
|
|
56
|
+
constructor(options: MockAgentOptions) {
|
|
57
|
+
super()
|
|
58
|
+
this.customAgent = options.customAgent
|
|
59
|
+
this.onRequest = options.onRequest
|
|
60
|
+
this.onResponse = options.onResponse
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public createConnection(options: any, callback: any) {
|
|
64
|
+
const createConnection =
|
|
65
|
+
(this.customAgent instanceof https.Agent &&
|
|
66
|
+
this.customAgent.createConnection) ||
|
|
67
|
+
super.createConnection
|
|
68
|
+
|
|
69
|
+
const socket = new MockHttpSocket({
|
|
70
|
+
connectionOptions: options,
|
|
71
|
+
createConnection: createConnection.bind(this, options, callback),
|
|
72
|
+
onRequest: this.onRequest.bind(this),
|
|
73
|
+
onResponse: this.onResponse.bind(this),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return socket
|
|
77
|
+
}
|
|
78
|
+
}
|