@mswjs/interceptors
Version:
Low-level HTTP/HTTPS/XHR/fetch request interception library.
198 lines (169 loc) • 5.48 kB
text/typescript
import http from 'node:http'
import https from 'node:https'
import { Interceptor } from '../../Interceptor'
import type { HttpRequestEventMap } from '../../glossary'
import {
kRequestId,
MockHttpSocketRequestCallback,
MockHttpSocketResponseCallback,
} from './MockHttpSocket'
import { MockAgent, MockHttpsAgent } from './agents'
import { RequestController } from '../../RequestController'
import { emitAsync } from '../../utils/emitAsync'
import { normalizeClientRequestArgs } from './utils/normalizeClientRequestArgs'
import { handleRequest } from '../../utils/handleRequest'
import {
recordRawFetchHeaders,
restoreHeadersPrototype,
} from './utils/recordRawHeaders'
export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
static symbol = Symbol('client-request-interceptor')
constructor() {
super(ClientRequestInterceptor.symbol)
}
protected setup(): void {
const {
ClientRequest: OriginalClientRequest,
get: originalGet,
request: originalRequest,
} = http
const { get: originalHttpsGet, request: originalHttpsRequest } = https
const onRequest = this.onRequest.bind(this)
const onResponse = this.onResponse.bind(this)
// Support requests performed via the `ClientRequest` constructor directly.
http.ClientRequest = new Proxy(http.ClientRequest, {
construct: (target, args: Parameters<typeof http.request>) => {
const [url, options, callback] = normalizeClientRequestArgs(
'http:',
args
)
// Create a mock agent instance appropriate for the request protocol.
const Agent = options.protocol === 'https:' ? MockHttpsAgent : MockAgent
const mockAgent = new Agent({
customAgent: options.agent,
onRequest,
onResponse,
})
options.agent = mockAgent
return Reflect.construct(target, [url, options, callback])
},
})
http.request = new Proxy(http.request, {
apply: (target, thisArg, args: Parameters<typeof http.request>) => {
const [url, options, callback] = normalizeClientRequestArgs(
'http:',
args
)
const mockAgent = new MockAgent({
customAgent: options.agent,
onRequest,
onResponse,
})
options.agent = mockAgent
return Reflect.apply(target, thisArg, [url, options, callback])
},
})
http.get = new Proxy(http.get, {
apply: (target, thisArg, args: Parameters<typeof http.get>) => {
const [url, options, callback] = normalizeClientRequestArgs(
'http:',
args
)
const mockAgent = new MockAgent({
customAgent: options.agent,
onRequest,
onResponse,
})
options.agent = mockAgent
return Reflect.apply(target, thisArg, [url, options, callback])
},
})
//
// HTTPS.
//
https.request = new Proxy(https.request, {
apply: (target, thisArg, args: Parameters<typeof https.request>) => {
const [url, options, callback] = normalizeClientRequestArgs(
'https:',
args
)
const mockAgent = new MockHttpsAgent({
customAgent: options.agent,
onRequest,
onResponse,
})
options.agent = mockAgent
return Reflect.apply(target, thisArg, [url, options, callback])
},
})
https.get = new Proxy(https.get, {
apply: (target, thisArg, args: Parameters<typeof https.get>) => {
const [url, options, callback] = normalizeClientRequestArgs(
'https:',
args
)
const mockAgent = new MockHttpsAgent({
customAgent: options.agent,
onRequest,
onResponse,
})
options.agent = mockAgent
return Reflect.apply(target, thisArg, [url, options, callback])
},
})
// Spy on `Header.prototype.set` and `Header.prototype.append` calls
// and record the raw header names provided. This is to support
// `IncomingMessage.prototype.rawHeaders`.
recordRawFetchHeaders()
this.subscriptions.push(() => {
http.ClientRequest = OriginalClientRequest
http.get = originalGet
http.request = originalRequest
https.get = originalHttpsGet
https.request = originalHttpsRequest
restoreHeadersPrototype()
})
}
private onRequest: MockHttpSocketRequestCallback = async ({
request,
socket,
}) => {
const requestId = Reflect.get(request, kRequestId)
const controller = new RequestController(request)
const isRequestHandled = await handleRequest({
request,
requestId,
controller,
emitter: this.emitter,
onResponse: (response) => {
socket.respondWith(response)
},
onRequestError: (response) => {
socket.respondWith(response)
},
onError: (error) => {
if (error instanceof Error) {
socket.errorWith(error)
}
},
})
if (!isRequestHandled) {
return socket.passthrough()
}
}
public onResponse: MockHttpSocketResponseCallback = async ({
requestId,
request,
response,
isMockedResponse,
}) => {
// Return the promise to when all the response event listeners
// are finished.
return emitAsync(this.emitter, 'response', {
requestId,
request,
response,
isMockedResponse,
})
}
}