Files
Shifted/node_modules/@mswjs/interceptors/lib/node/ClientRequest-Ca8Qykuv.mjs
2026-02-10 01:14:19 +00:00

1034 lines
38 KiB
JavaScript

import { i as RequestController, o as INTERNAL_REQUEST_ID_HEADER_NAME, r as createRequestId, s as Interceptor, t as FetchResponse } from "./fetchUtils-CoU35g3M.mjs";
import { n as setRawRequest } from "./getRawRequest-DnwmXyOW.mjs";
import { a as isPropertyAccessible, i as emitAsync, r as isObject, t as handleRequest } from "./handleRequest-Y97UwBbF.mjs";
import { n as setRawRequestBodyStream } from "./node-DwCc6iuP.mjs";
import { Logger } from "@open-draft/logger";
import { invariant } from "outvariant";
import http, { IncomingMessage, STATUS_CODES, ServerResponse, globalAgent } from "node:http";
import https, { globalAgent as globalAgent$1 } from "node:https";
import net from "node:net";
import { HTTPParser } from "_http_common";
import { Readable } from "node:stream";
import { URL as URL$1, parse, urlToHttpOptions } from "node:url";
import { Agent } from "http";
//#region src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts
/**
* Normalizes the arguments provided to the `Writable.prototype.write()`
* and `Writable.prototype.end()`.
*/
function normalizeSocketWriteArgs(args) {
const normalized = [
args[0],
void 0,
void 0
];
if (typeof args[1] === "string") normalized[1] = args[1];
else if (typeof args[1] === "function") normalized[2] = args[1];
if (typeof args[2] === "function") normalized[2] = args[2];
return normalized;
}
//#endregion
//#region src/interceptors/Socket/MockSocket.ts
var MockSocket = class extends net.Socket {
constructor(options) {
super();
this.options = options;
this.connecting = false;
this.connect();
this._final = (callback) => {
callback(null);
};
}
connect() {
this.connecting = true;
return this;
}
write(...args) {
const [chunk, encoding, callback] = normalizeSocketWriteArgs(args);
this.options.write(chunk, encoding, callback);
return true;
}
end(...args) {
const [chunk, encoding, callback] = normalizeSocketWriteArgs(args);
this.options.write(chunk, encoding, callback);
return super.end.apply(this, args);
}
push(chunk, encoding) {
this.options.read(chunk, encoding);
return super.push(chunk, encoding);
}
};
//#endregion
//#region src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts
function baseUrlFromConnectionOptions(options) {
if ("href" in options) return new URL(options.href);
const protocol = options.port === 443 ? "https:" : "http:";
const host = options.host;
const url = new URL(`${protocol}//${host}`);
if (options.port) url.port = options.port.toString();
if (options.path) url.pathname = options.path;
if (options.auth) {
const [username, password] = options.auth.split(":");
url.username = username;
url.password = password;
}
return url;
}
//#endregion
//#region src/interceptors/ClientRequest/utils/recordRawHeaders.ts
const kRawHeaders = Symbol("kRawHeaders");
const kRestorePatches = Symbol("kRestorePatches");
function recordRawHeader(headers, args, behavior) {
ensureRawHeadersSymbol(headers, []);
const rawHeaders = Reflect.get(headers, kRawHeaders);
if (behavior === "set") {
for (let index = rawHeaders.length - 1; index >= 0; index--) if (rawHeaders[index][0].toLowerCase() === args[0].toLowerCase()) rawHeaders.splice(index, 1);
}
rawHeaders.push(args);
}
/**
* Define the raw headers symbol on the given `Headers` instance.
* If the symbol already exists, this function does nothing.
*/
function ensureRawHeadersSymbol(headers, rawHeaders) {
if (Reflect.has(headers, kRawHeaders)) return;
defineRawHeadersSymbol(headers, rawHeaders);
}
/**
* Define the raw headers symbol on the given `Headers` instance.
* If the symbol already exists, it gets overridden.
*/
function defineRawHeadersSymbol(headers, rawHeaders) {
Object.defineProperty(headers, kRawHeaders, {
value: rawHeaders,
enumerable: false,
configurable: true
});
}
/**
* Patch the global `Headers` class to store raw headers.
* This is for compatibility with `IncomingMessage.prototype.rawHeaders`.
*
* @note Node.js has their own raw headers symbol but it
* only records the first header name in case of multi-value headers.
* Any other headers are normalized before comparing. This makes it
* incompatible with the `rawHeaders` format.
*
* let h = new Headers()
* h.append('X-Custom', 'one')
* h.append('x-custom', 'two')
* h[Symbol('headers map')] // Map { 'X-Custom' => 'one, two' }
*/
function recordRawFetchHeaders() {
if (Reflect.get(Headers, kRestorePatches)) return Reflect.get(Headers, kRestorePatches);
const { Headers: OriginalHeaders, Request: OriginalRequest, Response: OriginalResponse } = globalThis;
const { set, append, delete: headersDeleteMethod } = Headers.prototype;
Object.defineProperty(Headers, kRestorePatches, {
value: () => {
Headers.prototype.set = set;
Headers.prototype.append = append;
Headers.prototype.delete = headersDeleteMethod;
globalThis.Headers = OriginalHeaders;
globalThis.Request = OriginalRequest;
globalThis.Response = OriginalResponse;
Reflect.deleteProperty(Headers, kRestorePatches);
},
enumerable: false,
configurable: true
});
Object.defineProperty(globalThis, "Headers", {
enumerable: true,
writable: true,
value: new Proxy(Headers, { construct(target, args, newTarget) {
const headersInit = args[0] || [];
if (headersInit instanceof Headers && Reflect.has(headersInit, kRawHeaders)) {
const headers$1 = Reflect.construct(target, [Reflect.get(headersInit, kRawHeaders)], newTarget);
ensureRawHeadersSymbol(headers$1, [...Reflect.get(headersInit, kRawHeaders)]);
return headers$1;
}
const headers = Reflect.construct(target, args, newTarget);
if (!Reflect.has(headers, kRawHeaders)) ensureRawHeadersSymbol(headers, Array.isArray(headersInit) ? headersInit : Object.entries(headersInit));
return headers;
} })
});
Headers.prototype.set = new Proxy(Headers.prototype.set, { apply(target, thisArg, args) {
recordRawHeader(thisArg, args, "set");
return Reflect.apply(target, thisArg, args);
} });
Headers.prototype.append = new Proxy(Headers.prototype.append, { apply(target, thisArg, args) {
recordRawHeader(thisArg, args, "append");
return Reflect.apply(target, thisArg, args);
} });
Headers.prototype.delete = new Proxy(Headers.prototype.delete, { apply(target, thisArg, args) {
const rawHeaders = Reflect.get(thisArg, kRawHeaders);
if (rawHeaders) {
for (let index = rawHeaders.length - 1; index >= 0; index--) if (rawHeaders[index][0].toLowerCase() === args[0].toLowerCase()) rawHeaders.splice(index, 1);
}
return Reflect.apply(target, thisArg, args);
} });
Object.defineProperty(globalThis, "Request", {
enumerable: true,
writable: true,
value: new Proxy(Request, { construct(target, args, newTarget) {
const request = Reflect.construct(target, args, newTarget);
const inferredRawHeaders = [];
if (typeof args[0] === "object" && args[0].headers != null) inferredRawHeaders.push(...inferRawHeaders(args[0].headers));
if (typeof args[1] === "object" && args[1].headers != null) inferredRawHeaders.push(...inferRawHeaders(args[1].headers));
if (inferredRawHeaders.length > 0) ensureRawHeadersSymbol(request.headers, inferredRawHeaders);
return request;
} })
});
Object.defineProperty(globalThis, "Response", {
enumerable: true,
writable: true,
value: new Proxy(Response, { construct(target, args, newTarget) {
const response = Reflect.construct(target, args, newTarget);
if (typeof args[1] === "object" && args[1].headers != null) ensureRawHeadersSymbol(response.headers, inferRawHeaders(args[1].headers));
return response;
} })
});
}
function restoreHeadersPrototype() {
if (!Reflect.get(Headers, kRestorePatches)) return;
Reflect.get(Headers, kRestorePatches)();
}
function getRawFetchHeaders(headers) {
if (!Reflect.has(headers, kRawHeaders)) return Array.from(headers.entries());
const rawHeaders = Reflect.get(headers, kRawHeaders);
return rawHeaders.length > 0 ? rawHeaders : Array.from(headers.entries());
}
/**
* Infers the raw headers from the given `HeadersInit` provided
* to the Request/Response constructor.
*
* If the `init.headers` is a Headers instance, use it directly.
* That means the headers were created standalone and already have
* the raw headers stored.
* If the `init.headers` is a HeadersInit, create a new Headers
* instance out of it.
*/
function inferRawHeaders(headers) {
if (headers instanceof Headers) return Reflect.get(headers, kRawHeaders) || [];
return Reflect.get(new Headers(headers), kRawHeaders);
}
//#endregion
//#region src/interceptors/ClientRequest/utils/parserUtils.ts
/**
* @see https://github.com/nodejs/node/blob/f3adc11e37b8bfaaa026ea85c1cf22e3a0e29ae9/lib/_http_common.js#L180
*/
function freeParser(parser, socket) {
if (parser._consumed) parser.unconsume();
parser._headers = [];
parser._url = "";
parser.socket = null;
parser.incoming = null;
parser.outgoing = null;
parser.maxHeaderPairs = 2e3;
parser._consumed = false;
parser.onIncoming = null;
parser[HTTPParser.kOnHeaders] = null;
parser[HTTPParser.kOnHeadersComplete] = null;
parser[HTTPParser.kOnMessageBegin] = null;
parser[HTTPParser.kOnMessageComplete] = null;
parser[HTTPParser.kOnBody] = null;
parser[HTTPParser.kOnExecute] = null;
parser[HTTPParser.kOnTimeout] = null;
parser.remove();
parser.free();
if (socket)
/**
* @note Unassigning the socket's parser will fail this assertion
* if there's still some data being processed on the socket:
* @see https://github.com/nodejs/node/blob/4e1f39b678b37017ac9baa0971e3aeecd3b67b51/lib/_http_client.js#L613
*/
if (socket.destroyed) socket.parser = null;
else socket.once("end", () => {
socket.parser = null;
});
}
//#endregion
//#region src/interceptors/ClientRequest/MockHttpSocket.ts
const kRequestId = Symbol("kRequestId");
var MockHttpSocket = class extends MockSocket {
constructor(options) {
super({
write: (chunk, encoding, callback) => {
if (this.socketState !== "passthrough") this.writeBuffer.push([
chunk,
encoding,
callback
]);
if (chunk) {
/**
* Forward any writes to the mock socket to the underlying original socket.
* This ensures functional duplex connections, like WebSocket.
* @see https://github.com/mswjs/interceptors/issues/682
*/
if (this.socketState === "passthrough") this.originalSocket?.write(chunk, encoding, callback);
this.requestParser.execute(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding));
}
},
read: (chunk) => {
if (chunk !== null)
/**
* @todo We need to free the parser if the connection has been
* upgraded to a non-HTTP protocol. It won't be able to parse data
* from that point onward anyway. No need to keep it in memory.
*/
this.responseParser.execute(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
});
this.requestRawHeadersBuffer = [];
this.responseRawHeadersBuffer = [];
this.writeBuffer = [];
this.socketState = "unknown";
this.onRequestHeaders = (rawHeaders) => {
this.requestRawHeadersBuffer.push(...rawHeaders);
};
this.onRequestStart = (versionMajor, versionMinor, rawHeaders, _, path, __, ___, ____, shouldKeepAlive) => {
this.shouldKeepAlive = shouldKeepAlive;
const url = new URL(path || "", this.baseUrl);
const method = this.connectionOptions.method?.toUpperCase() || "GET";
const headers = FetchResponse.parseRawHeaders([...this.requestRawHeadersBuffer, ...rawHeaders || []]);
this.requestRawHeadersBuffer.length = 0;
const canHaveBody = method !== "GET" && method !== "HEAD";
if (url.username || url.password) {
if (!headers.has("authorization")) headers.set("authorization", `Basic ${url.username}:${url.password}`);
url.username = "";
url.password = "";
}
this.requestStream = new Readable({ read: () => {
this.flushWriteBuffer();
} });
const requestId = createRequestId();
this.request = new Request(url, {
method,
headers,
credentials: "same-origin",
duplex: canHaveBody ? "half" : void 0,
body: canHaveBody ? Readable.toWeb(this.requestStream) : null
});
Reflect.set(this.request, kRequestId, requestId);
setRawRequest(this.request, Reflect.get(this, "_httpMessage"));
setRawRequestBodyStream(this.request, this.requestStream);
/**
* @fixme Stop relying on the "X-Request-Id" request header
* to figure out if one interceptor has been invoked within another.
* @see https://github.com/mswjs/interceptors/issues/378
*/
if (this.request.headers.has(INTERNAL_REQUEST_ID_HEADER_NAME)) {
this.passthrough();
return;
}
this.onRequest({
requestId,
request: this.request,
socket: this
});
};
this.onResponseHeaders = (rawHeaders) => {
this.responseRawHeadersBuffer.push(...rawHeaders);
};
this.onResponseStart = (versionMajor, versionMinor, rawHeaders, method, url, status, statusText) => {
const headers = FetchResponse.parseRawHeaders([...this.responseRawHeadersBuffer, ...rawHeaders || []]);
this.responseRawHeadersBuffer.length = 0;
const response = new FetchResponse(
/**
* @note The Fetch API response instance exposed to the consumer
* is created over the response stream of the HTTP parser. It is NOT
* related to the Socket instance. This way, you can read response body
* in response listener while the Socket instance delays the emission
* of "end" and other events until those response listeners are finished.
*/
FetchResponse.isResponseWithBody(status) ? Readable.toWeb(this.responseStream = new Readable({ read() {} })) : null,
{
url,
status,
statusText,
headers
}
);
invariant(this.request, "Failed to handle a response: request does not exist");
FetchResponse.setUrl(this.request.url, response);
/**
* @fixme Stop relying on the "X-Request-Id" request header
* to figure out if one interceptor has been invoked within another.
* @see https://github.com/mswjs/interceptors/issues/378
*/
if (this.request.headers.has(INTERNAL_REQUEST_ID_HEADER_NAME)) return;
this.responseListenersPromise = this.onResponse({
response,
isMockedResponse: this.socketState === "mock",
requestId: Reflect.get(this.request, kRequestId),
request: this.request,
socket: this
});
};
this.connectionOptions = options.connectionOptions;
this.createConnection = options.createConnection;
this.onRequest = options.onRequest;
this.onResponse = options.onResponse;
this.baseUrl = baseUrlFromConnectionOptions(this.connectionOptions);
this.requestParser = new HTTPParser();
this.requestParser.initialize(HTTPParser.REQUEST, {});
this.requestParser[HTTPParser.kOnHeaders] = this.onRequestHeaders.bind(this);
this.requestParser[HTTPParser.kOnHeadersComplete] = this.onRequestStart.bind(this);
this.requestParser[HTTPParser.kOnBody] = this.onRequestBody.bind(this);
this.requestParser[HTTPParser.kOnMessageComplete] = this.onRequestEnd.bind(this);
this.responseParser = new HTTPParser();
this.responseParser.initialize(HTTPParser.RESPONSE, {});
this.responseParser[HTTPParser.kOnHeaders] = this.onResponseHeaders.bind(this);
this.responseParser[HTTPParser.kOnHeadersComplete] = this.onResponseStart.bind(this);
this.responseParser[HTTPParser.kOnBody] = this.onResponseBody.bind(this);
this.responseParser[HTTPParser.kOnMessageComplete] = this.onResponseEnd.bind(this);
this.once("finish", () => freeParser(this.requestParser, this));
if (this.baseUrl.protocol === "https:") {
Reflect.set(this, "encrypted", true);
Reflect.set(this, "authorized", false);
Reflect.set(this, "getProtocol", () => "TLSv1.3");
Reflect.set(this, "getSession", () => void 0);
Reflect.set(this, "isSessionReused", () => false);
Reflect.set(this, "getCipher", () => ({
name: "AES256-SHA",
standardName: "TLS_RSA_WITH_AES_256_CBC_SHA",
version: "TLSv1.3"
}));
}
}
emit(event, ...args) {
const emitEvent = super.emit.bind(this, event, ...args);
if (this.responseListenersPromise) {
this.responseListenersPromise.finally(emitEvent);
return this.listenerCount(event) > 0;
}
return emitEvent();
}
destroy(error) {
freeParser(this.responseParser, this);
if (error) this.emit("error", error);
return super.destroy(error);
}
/**
* Establish this Socket connection as-is and pipe
* its data/events through this Socket.
*/
passthrough() {
this.socketState = "passthrough";
if (this.destroyed) return;
const socket = this.createConnection();
this.originalSocket = socket;
/**
* @note Inherit the original socket's connection handle.
* Without this, each push to the mock socket results in a
* new "connection" listener being added (i.e. buffering pushes).
* @see https://github.com/nodejs/node/blob/b18153598b25485ce4f54d0c5cb830a9457691ee/lib/net.js#L734
*/
if ("_handle" in socket) Object.defineProperty(this, "_handle", {
value: socket._handle,
enumerable: true,
writable: true
});
this.once("close", () => {
socket.removeAllListeners();
if (!socket.destroyed) socket.destroy();
this.originalSocket = void 0;
});
this.address = socket.address.bind(socket);
let writeArgs;
let headersWritten = false;
while (writeArgs = this.writeBuffer.shift()) if (writeArgs !== void 0) {
if (!headersWritten) {
const [chunk, encoding, callback] = writeArgs;
const chunkString = chunk.toString();
const chunkBeforeRequestHeaders = chunkString.slice(0, chunkString.indexOf("\r\n") + 2);
const chunkAfterRequestHeaders = chunkString.slice(chunk.indexOf("\r\n\r\n"));
const headersChunk = `${chunkBeforeRequestHeaders}${getRawFetchHeaders(this.request.headers).filter(([name]) => {
return name.toLowerCase() !== INTERNAL_REQUEST_ID_HEADER_NAME;
}).map(([name, value]) => `${name}: ${value}`).join("\r\n")}${chunkAfterRequestHeaders}`;
socket.write(headersChunk, encoding, callback);
headersWritten = true;
continue;
}
socket.write(...writeArgs);
}
if (Reflect.get(socket, "encrypted")) [
"encrypted",
"authorized",
"getProtocol",
"getSession",
"isSessionReused",
"getCipher"
].forEach((propertyName) => {
Object.defineProperty(this, propertyName, {
enumerable: true,
get: () => {
const value = Reflect.get(socket, propertyName);
return typeof value === "function" ? value.bind(socket) : value;
}
});
});
socket.on("lookup", (...args) => this.emit("lookup", ...args)).on("connect", () => {
this.connecting = socket.connecting;
this.emit("connect");
}).on("secureConnect", () => this.emit("secureConnect")).on("secure", () => this.emit("secure")).on("session", (session) => this.emit("session", session)).on("ready", () => this.emit("ready")).on("drain", () => this.emit("drain")).on("data", (chunk) => {
this.push(chunk);
}).on("error", (error) => {
Reflect.set(this, "_hadError", Reflect.get(socket, "_hadError"));
this.emit("error", error);
}).on("resume", () => this.emit("resume")).on("timeout", () => this.emit("timeout")).on("prefinish", () => this.emit("prefinish")).on("finish", () => this.emit("finish")).on("close", (hadError) => this.emit("close", hadError)).on("end", () => this.emit("end"));
}
/**
* Convert the given Fetch API `Response` instance to an
* HTTP message and push it to the socket.
*/
async respondWith(response) {
if (this.destroyed) return;
invariant(this.socketState !== "mock", "[MockHttpSocket] Failed to respond to the \"%s %s\" request with \"%s %s\": the request has already been handled", this.request?.method, this.request?.url, response.status, response.statusText);
if (isPropertyAccessible(response, "type") && response.type === "error") {
this.errorWith(/* @__PURE__ */ new TypeError("Network error"));
return;
}
this.mockConnect();
this.socketState = "mock";
this.flushWriteBuffer();
const serverResponse = new ServerResponse(new IncomingMessage(this));
/**
* Assign a mock socket instance to the server response to
* spy on the response chunk writes. Push the transformed response chunks
* to this `MockHttpSocket` instance to trigger the "data" event.
* @note Providing the same `MockSocket` instance when creating `ServerResponse`
* does not have the same effect.
* @see https://github.com/nodejs/node/blob/10099bb3f7fd97bb9dd9667188426866b3098e07/test/parallel/test-http-server-response-standalone.js#L32
*/
serverResponse.assignSocket(new MockSocket({
write: (chunk, encoding, callback) => {
this.push(chunk, encoding);
callback?.();
},
read() {}
}));
/**
* @note Remove the `Connection` and `Date` response headers
* injected by `ServerResponse` by default. Those are required
* from the server but the interceptor is NOT technically a server.
* It's confusing to add response headers that the developer didn't
* specify themselves. They can always add these if they wish.
* @see https://www.rfc-editor.org/rfc/rfc9110#field.date
* @see https://www.rfc-editor.org/rfc/rfc9110#field.connection
*/
serverResponse.removeHeader("connection");
serverResponse.removeHeader("date");
const rawResponseHeaders = getRawFetchHeaders(response.headers);
/**
* @note Call `.writeHead` in order to set the raw response headers
* in the same case as they were provided by the developer. Using
* `.setHeader()`/`.appendHeader()` normalizes header names.
*/
serverResponse.writeHead(response.status, response.statusText || STATUS_CODES[response.status], rawResponseHeaders);
this.once("error", () => {
serverResponse.destroy();
});
if (response.body) try {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
serverResponse.end();
break;
}
serverResponse.write(value);
}
} catch (error) {
if (error instanceof Error) {
serverResponse.destroy();
/**
* @note Destroy the request socket gracefully.
* Response stream errors do NOT produce request errors.
*/
this.destroy();
return;
}
serverResponse.destroy();
throw error;
}
else serverResponse.end();
if (!this.shouldKeepAlive) {
this.emit("readable");
/**
* @todo @fixme This is likely a hack.
* Since we push null to the socket, it never propagates to the
* parser, and the parser never calls "onResponseEnd" to close
* the response stream. We are closing the stream here manually
* but that shouldn't be the case.
*/
this.responseStream?.push(null);
this.push(null);
}
}
/**
* Close this socket connection with the given error.
*/
errorWith(error) {
this.destroy(error);
}
mockConnect() {
this.connecting = false;
const isIPv6 = net.isIPv6(this.connectionOptions.hostname) || this.connectionOptions.family === 6;
const addressInfo = {
address: isIPv6 ? "::1" : "127.0.0.1",
family: isIPv6 ? "IPv6" : "IPv4",
port: this.connectionOptions.port
};
this.address = () => addressInfo;
this.emit("lookup", null, addressInfo.address, addressInfo.family === "IPv6" ? 6 : 4, this.connectionOptions.host);
this.emit("connect");
this.emit("ready");
if (this.baseUrl.protocol === "https:") {
this.emit("secure");
this.emit("secureConnect");
this.emit("session", this.connectionOptions.session || Buffer.from("mock-session-renegotiate"));
this.emit("session", Buffer.from("mock-session-resume"));
}
}
flushWriteBuffer() {
for (const writeCall of this.writeBuffer) if (typeof writeCall[2] === "function") {
writeCall[2]();
/**
* @note Remove the callback from the write call
* so it doesn't get called twice on passthrough
* if `request.end()` was called within `request.write()`.
* @see https://github.com/mswjs/interceptors/issues/684
*/
writeCall[2] = void 0;
}
}
onRequestBody(chunk) {
invariant(this.requestStream, "Failed to write to a request stream: stream does not exist");
this.requestStream.push(chunk);
}
onRequestEnd() {
if (this.requestStream) this.requestStream.push(null);
}
onResponseBody(chunk) {
invariant(this.responseStream, "Failed to write to a response stream: stream does not exist");
this.responseStream.push(chunk);
}
onResponseEnd() {
if (this.responseStream) this.responseStream.push(null);
}
};
//#endregion
//#region src/interceptors/ClientRequest/agents.ts
var MockAgent = class extends http.Agent {
constructor(options) {
super();
this.customAgent = options.customAgent;
this.onRequest = options.onRequest;
this.onResponse = options.onResponse;
}
createConnection(options, callback) {
const createConnection = this.customAgent instanceof http.Agent ? this.customAgent.createConnection : super.createConnection;
const createConnectionOptions = this.customAgent instanceof http.Agent ? {
...options,
...this.customAgent.options
} : options;
return new MockHttpSocket({
connectionOptions: options,
createConnection: createConnection.bind(this.customAgent || this, createConnectionOptions, callback),
onRequest: this.onRequest.bind(this),
onResponse: this.onResponse.bind(this)
});
}
};
var MockHttpsAgent = class extends https.Agent {
constructor(options) {
super();
this.customAgent = options.customAgent;
this.onRequest = options.onRequest;
this.onResponse = options.onResponse;
}
createConnection(options, callback) {
const createConnection = this.customAgent instanceof http.Agent ? this.customAgent.createConnection : super.createConnection;
const createConnectionOptions = this.customAgent instanceof http.Agent ? {
...options,
...this.customAgent.options
} : options;
return new MockHttpSocket({
connectionOptions: options,
createConnection: createConnection.bind(this.customAgent || this, createConnectionOptions, callback),
onRequest: this.onRequest.bind(this),
onResponse: this.onResponse.bind(this)
});
}
};
//#endregion
//#region src/utils/getUrlByRequestOptions.ts
const logger$2 = new Logger("utils getUrlByRequestOptions");
const DEFAULT_PATH = "/";
const DEFAULT_PROTOCOL = "http:";
const DEFAULT_HOSTNAME = "localhost";
const SSL_PORT = 443;
function getAgent(options) {
return options.agent instanceof Agent ? options.agent : void 0;
}
function getProtocolByRequestOptions(options) {
if (options.protocol) return options.protocol;
const agentProtocol = getAgent(options)?.protocol;
if (agentProtocol) return agentProtocol;
const port = getPortByRequestOptions(options);
return options.cert || port === SSL_PORT ? "https:" : options.uri?.protocol || DEFAULT_PROTOCOL;
}
function getPortByRequestOptions(options) {
if (options.port) return Number(options.port);
const agent = getAgent(options);
if (agent?.options.port) return Number(agent.options.port);
if (agent?.defaultPort) return Number(agent.defaultPort);
}
function getAuthByRequestOptions(options) {
if (options.auth) {
const [username, password] = options.auth.split(":");
return {
username,
password
};
}
}
/**
* Returns true if host looks like an IPv6 address without surrounding brackets
* It assumes any host containing `:` is definitely not IPv4 and probably IPv6,
* but note that this could include invalid IPv6 addresses as well.
*/
function isRawIPv6Address(host) {
return host.includes(":") && !host.startsWith("[") && !host.endsWith("]");
}
function getHostname(options) {
let host = options.hostname || options.host;
if (host) {
if (isRawIPv6Address(host)) host = `[${host}]`;
return new URL(`http://${host}`).hostname;
}
return DEFAULT_HOSTNAME;
}
/**
* Creates a `URL` instance from a given `RequestOptions` object.
*/
function getUrlByRequestOptions(options) {
logger$2.info("request options", options);
if (options.uri) {
logger$2.info("constructing url from explicitly provided \"options.uri\": %s", options.uri);
return new URL(options.uri.href);
}
logger$2.info("figuring out url from request options...");
const protocol = getProtocolByRequestOptions(options);
logger$2.info("protocol", protocol);
const port = getPortByRequestOptions(options);
logger$2.info("port", port);
const hostname = getHostname(options);
logger$2.info("hostname", hostname);
const path = options.path || DEFAULT_PATH;
logger$2.info("path", path);
const credentials = getAuthByRequestOptions(options);
logger$2.info("credentials", credentials);
const authString = credentials ? `${credentials.username}:${credentials.password}@` : "";
logger$2.info("auth string:", authString);
const portString = typeof port !== "undefined" ? `:${port}` : "";
const url = new URL(`${protocol}//${hostname}${portString}${path}`);
url.username = credentials?.username || "";
url.password = credentials?.password || "";
logger$2.info("created url:", url);
return url;
}
//#endregion
//#region src/utils/cloneObject.ts
const logger$1 = new Logger("cloneObject");
function isPlainObject(obj) {
logger$1.info("is plain object?", obj);
if (obj == null || !obj.constructor?.name) {
logger$1.info("given object is undefined, not a plain object...");
return false;
}
logger$1.info("checking the object constructor:", obj.constructor.name);
return obj.constructor.name === "Object";
}
function cloneObject(obj) {
logger$1.info("cloning object:", obj);
const enumerableProperties = Object.entries(obj).reduce((acc, [key, value]) => {
logger$1.info("analyzing key-value pair:", key, value);
acc[key] = isPlainObject(value) ? cloneObject(value) : value;
return acc;
}, {});
return isPlainObject(obj) ? enumerableProperties : Object.assign(Object.getPrototypeOf(obj), enumerableProperties);
}
//#endregion
//#region src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts
const logger = new Logger("http normalizeClientRequestArgs");
function resolveRequestOptions(args, url) {
if (typeof args[1] === "undefined" || typeof args[1] === "function") {
logger.info("request options not provided, deriving from the url", url);
return urlToHttpOptions(url);
}
if (args[1]) {
logger.info("has custom RequestOptions!", args[1]);
const requestOptionsFromUrl = urlToHttpOptions(url);
logger.info("derived RequestOptions from the URL:", requestOptionsFromUrl);
/**
* Clone the request options to lock their state
* at the moment they are provided to `ClientRequest`.
* @see https://github.com/mswjs/interceptors/issues/86
*/
logger.info("cloning RequestOptions...");
const clonedRequestOptions = cloneObject(args[1]);
logger.info("successfully cloned RequestOptions!", clonedRequestOptions);
return {
...requestOptionsFromUrl,
...clonedRequestOptions
};
}
logger.info("using an empty object as request options");
return {};
}
/**
* Overrides the given `URL` instance with the explicit properties provided
* on the `RequestOptions` object. The options object takes precedence,
* and will replace URL properties like "host", "path", and "port", if specified.
*/
function overrideUrlByRequestOptions(url, options) {
url.host = options.host || url.host;
url.hostname = options.hostname || url.hostname;
url.port = options.port ? options.port.toString() : url.port;
if (options.path) {
const parsedOptionsPath = parse(options.path, false);
url.pathname = parsedOptionsPath.pathname || "";
url.search = parsedOptionsPath.search || "";
}
return url;
}
function resolveCallback(args) {
return typeof args[1] === "function" ? args[1] : args[2];
}
/**
* Normalizes parameters given to a `http.request` call
* so it always has a `URL` and `RequestOptions`.
*/
function normalizeClientRequestArgs(defaultProtocol, args) {
let url;
let options;
let callback;
logger.info("arguments", args);
logger.info("using default protocol:", defaultProtocol);
if (args.length === 0) {
const url$1 = new URL$1("http://localhost");
return [url$1, resolveRequestOptions(args, url$1)];
}
if (typeof args[0] === "string") {
logger.info("first argument is a location string:", args[0]);
url = new URL$1(args[0]);
logger.info("created a url:", url);
const requestOptionsFromUrl = urlToHttpOptions(url);
logger.info("request options from url:", requestOptionsFromUrl);
options = resolveRequestOptions(args, url);
logger.info("resolved request options:", options);
callback = resolveCallback(args);
} else if (args[0] instanceof URL$1) {
url = args[0];
logger.info("first argument is a URL:", url);
if (typeof args[1] !== "undefined" && isObject(args[1])) url = overrideUrlByRequestOptions(url, args[1]);
options = resolveRequestOptions(args, url);
logger.info("derived request options:", options);
callback = resolveCallback(args);
} else if ("hash" in args[0] && !("method" in args[0])) {
const [legacyUrl] = args;
logger.info("first argument is a legacy URL:", legacyUrl);
if (legacyUrl.hostname === null) {
/**
* We are dealing with a relative url, so use the path as an "option" and
* merge in any existing options, giving priority to existing options -- i.e. a path in any
* existing options will take precedence over the one contained in the url. This is consistent
* with the behaviour in ClientRequest.
* @see https://github.com/nodejs/node/blob/d84f1312915fe45fe0febe888db692c74894c382/lib/_http_client.js#L122
*/
logger.info("given legacy URL is relative (no hostname)");
return isObject(args[1]) ? normalizeClientRequestArgs(defaultProtocol, [{
path: legacyUrl.path,
...args[1]
}, args[2]]) : normalizeClientRequestArgs(defaultProtocol, [{ path: legacyUrl.path }, args[1]]);
}
logger.info("given legacy url is absolute");
const resolvedUrl = new URL$1(legacyUrl.href);
return args[1] === void 0 ? normalizeClientRequestArgs(defaultProtocol, [resolvedUrl]) : typeof args[1] === "function" ? normalizeClientRequestArgs(defaultProtocol, [resolvedUrl, args[1]]) : normalizeClientRequestArgs(defaultProtocol, [
resolvedUrl,
args[1],
args[2]
]);
} else if (isObject(args[0])) {
options = { ...args[0] };
logger.info("first argument is RequestOptions:", options);
options.protocol = options.protocol || defaultProtocol;
logger.info("normalized request options:", options);
url = getUrlByRequestOptions(options);
logger.info("created a URL from RequestOptions:", url.href);
callback = resolveCallback(args);
} else throw new Error(`Failed to construct ClientRequest with these parameters: ${args}`);
options.protocol = options.protocol || url.protocol;
options.method = options.method || "GET";
/**
* Ensure that the default Agent is always set.
* This prevents the protocol mismatch for requests with { agent: false },
* where the global Agent is inferred.
* @see https://github.com/mswjs/msw/issues/1150
* @see https://github.com/nodejs/node/blob/418ff70b810f0e7112d48baaa72932a56cfa213b/lib/_http_client.js#L130
* @see https://github.com/nodejs/node/blob/418ff70b810f0e7112d48baaa72932a56cfa213b/lib/_http_client.js#L157-L159
*/
if (!options._defaultAgent) {
logger.info("has no default agent, setting the default agent for \"%s\"", options.protocol);
options._defaultAgent = options.protocol === "https:" ? globalAgent$1 : globalAgent;
}
logger.info("successfully resolved url:", url.href);
logger.info("successfully resolved options:", options);
logger.info("successfully resolved callback:", callback);
/**
* @note If the user-provided URL is not a valid URL in Node.js,
* (e.g. the one provided by the JSDOM polyfills), case it to
* string. Otherwise, this throws on Node.js incompatibility
* (`ERR_INVALID_ARG_TYPE` on the connection listener)
* @see https://github.com/node-fetch/node-fetch/issues/1376#issuecomment-966435555
*/
if (!(url instanceof URL$1)) url = url.toString();
return [
url,
options,
callback
];
}
//#endregion
//#region src/interceptors/ClientRequest/index.ts
var ClientRequestInterceptor = class ClientRequestInterceptor extends Interceptor {
static {
this.symbol = Symbol("client-request-interceptor");
}
constructor() {
super(ClientRequestInterceptor.symbol);
this.onRequest = async ({ request, socket }) => {
const controller = new RequestController(request, {
passthrough() {
socket.passthrough();
},
async respondWith(response) {
await socket.respondWith(response);
},
errorWith(reason) {
if (reason instanceof Error) socket.errorWith(reason);
}
});
await handleRequest({
request,
requestId: Reflect.get(request, kRequestId),
controller,
emitter: this.emitter
});
};
this.onResponse = async ({ requestId, request, response, isMockedResponse }) => {
return emitAsync(this.emitter, "response", {
requestId,
request,
response,
isMockedResponse
});
};
}
setup() {
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);
http.ClientRequest = new Proxy(http.ClientRequest, { construct: (target, args) => {
const [url, options, callback] = normalizeClientRequestArgs("http:", args);
options.agent = new (options.protocol === "https:" ? MockHttpsAgent : MockAgent)({
customAgent: options.agent,
onRequest,
onResponse
});
return Reflect.construct(target, [
url,
options,
callback
]);
} });
http.request = new Proxy(http.request, { apply: (target, thisArg, args) => {
const [url, options, callback] = normalizeClientRequestArgs("http:", args);
options.agent = new MockAgent({
customAgent: options.agent,
onRequest,
onResponse
});
return Reflect.apply(target, thisArg, [
url,
options,
callback
]);
} });
http.get = new Proxy(http.get, { apply: (target, thisArg, args) => {
const [url, options, callback] = normalizeClientRequestArgs("http:", args);
options.agent = new MockAgent({
customAgent: options.agent,
onRequest,
onResponse
});
return Reflect.apply(target, thisArg, [
url,
options,
callback
]);
} });
https.request = new Proxy(https.request, { apply: (target, thisArg, args) => {
const [url, options, callback] = normalizeClientRequestArgs("https:", args);
options.agent = new MockHttpsAgent({
customAgent: options.agent,
onRequest,
onResponse
});
return Reflect.apply(target, thisArg, [
url,
options,
callback
]);
} });
https.get = new Proxy(https.get, { apply: (target, thisArg, args) => {
const [url, options, callback] = normalizeClientRequestArgs("https:", args);
options.agent = new MockHttpsAgent({
customAgent: options.agent,
onRequest,
onResponse
});
return Reflect.apply(target, thisArg, [
url,
options,
callback
]);
} });
recordRawFetchHeaders();
this.subscriptions.push(() => {
http.ClientRequest = OriginalClientRequest;
http.get = originalGet;
http.request = originalRequest;
https.get = originalHttpsGet;
https.request = originalHttpsRequest;
restoreHeadersPrototype();
});
}
};
//#endregion
export { ClientRequestInterceptor as t };
//# sourceMappingURL=ClientRequest-Ca8Qykuv.mjs.map