🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@mswjs/interceptors

Package Overview
Dependencies
Maintainers
1
Versions
197
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mswjs/interceptors - npm Package Compare versions

Comparing version
0.42.2
to
0.42.3
+22
lib/node/has-configurable-global-BU1sT1u4.js
import { a as getDeepPropertyDescriptor } from "./net-Ca8p1rIe.js";
//#region src/utils/has-configurable-global.ts
/**
* Returns a boolean indicating whether the given global property
* is defined and is configurable.
*/
function hasConfigurableGlobal(propertyName) {
const match = getDeepPropertyDescriptor(globalThis, propertyName);
if (typeof match === "undefined") return false;
const { descriptor } = match;
if (typeof descriptor.get === "function" && typeof descriptor.get() === "undefined") return false;
if (typeof descriptor.get === "undefined" && descriptor.value == null) return false;
if (typeof descriptor.set === "undefined" && !descriptor.configurable) {
console.error(`[MSW] Failed to apply interceptor: the global \`${propertyName}\` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.`);
return false;
}
return true;
}
//#endregion
export { hasConfigurableGlobal as t };
//# sourceMappingURL=has-configurable-global-BU1sT1u4.js.map
{"version":3,"file":"has-configurable-global-BU1sT1u4.js","names":[],"sources":["../../src/utils/has-configurable-global.ts"],"sourcesContent":["import { getDeepPropertyDescriptor } from './patches-registry'\n\n/**\n * Returns a boolean indicating whether the given global property\n * is defined and is configurable.\n */\nexport function hasConfigurableGlobal(propertyName: string): boolean {\n const match = getDeepPropertyDescriptor(globalThis, propertyName)\n\n // The property is not set at all.\n if (typeof match === 'undefined') {\n return false\n }\n\n const { descriptor } = match\n\n // The property is set to a getter that returns undefined.\n if (\n typeof descriptor.get === 'function' &&\n typeof descriptor.get() === 'undefined'\n ) {\n return false\n }\n\n // The property is set to a value equal to undefined.\n if (typeof descriptor.get === 'undefined' && descriptor.value == null) {\n return false\n }\n\n if (typeof descriptor.set === 'undefined' && !descriptor.configurable) {\n console.error(\n `[MSW] Failed to apply interceptor: the global \\`${propertyName}\\` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.`\n )\n return false\n }\n\n return true\n}\n"],"mappings":";;;;;;AAMA,SAAgB,sBAAsB,cAA+B;CACnE,MAAM,QAAQ,0BAA0B,YAAY,YAAY;CAGhE,IAAI,OAAO,UAAU,aACnB,OAAO;CAGT,MAAM,EAAE,eAAe;CAGvB,IACE,OAAO,WAAW,QAAQ,cAC1B,OAAO,WAAW,IAAI,MAAM,aAE5B,OAAO;CAIT,IAAI,OAAO,WAAW,QAAQ,eAAe,WAAW,SAAS,MAC/D,OAAO;CAGT,IAAI,OAAO,WAAW,QAAQ,eAAe,CAAC,WAAW,cAAc;EACrE,QAAQ,MACN,mDAAmD,aAAa,mKAClE;EACA,OAAO;CACT;CAEA,OAAO;AACT"}
import { a as createLogger, i as Interceptor, r as toBuffer } from "./buffer-utils-BvPY1Tc-.js";
import { TypedEvent } from "rettime";
import { invariant } from "outvariant";
import http from "node:http";
import net from "node:net";
import tls from "node:tls";
//#region src/utils/patches-registry.ts
var PatchesRegistry = class {
#replacements = /* @__PURE__ */ new Map();
applyPatch(owner, key, getNextValue) {
const ownerReplacements = this.#replacements.get(owner);
invariant(!ownerReplacements?.has(key), `Failed to replace a global value at "${String(key)}": already replaced.`);
const match = getDeepPropertyDescriptor(owner, key);
if (typeof match === "undefined") {
console.warn(`Failed to replace a global value at "${String(key)}": not a global value.`);
return () => {};
}
if (match.descriptor.configurable) Object.defineProperty(owner, key, {
value: getNextValue(owner[key]),
enumerable: true,
configurable: true
});
else if (match.descriptor.writable) owner[key] = getNextValue(owner[key]);
else throw new Error(`Failed to patch a non-configurable non-writable property "${key.toString()}"`);
const restorePatch = () => {
const currentReplacements = this.#replacements.get(owner);
if (!currentReplacements?.has(key)) return;
if (match.owner === owner)
/**
* @note Restoring non-configurable properties works as long as "writable: true"
* and none of the other descriptor properties except for "value" have changed.
*/
Object.defineProperty(match.owner, key, match.descriptor);
else
/**
* @todo Delete the proxy property set by the registry.
* If the match's owner isn't the original owner, the property is likely nested in the prototype.
* The registry does not meddle with those, they are left intact.
*/
Reflect.deleteProperty(owner, key);
currentReplacements.delete(key);
if (currentReplacements.size === 0) this.#replacements.delete(owner);
};
if (ownerReplacements) ownerReplacements.set(key, restorePatch);
else this.#replacements.set(owner, /* @__PURE__ */ new Map([[key, restorePatch]]));
return restorePatch;
}
restoreAllPatches() {
const errors = [];
for (const [, ownerReplacements] of this.#replacements) for (const [, restorePatch] of ownerReplacements) try {
restorePatch();
} catch (error) {
if (error instanceof Error) errors.push(error);
else throw error;
}
if (errors.length > 0) throw new AggregateError(errors, "FOO!");
}
};
const patchesRegistry = new PatchesRegistry();
/**
* Returns a property descriptor for the given property on the owner.
* Walks down the prototype chain if the property does not exist on the owner.
* Handy for getting a global property descriptor where `globalThis` is
* replaced with a controlled class (e.g. ServiceWorkerGlobalScope).
*/
function getDeepPropertyDescriptor(owner, key) {
let currentOwner = owner;
let descriptor;
while (currentOwner) {
descriptor = Object.getOwnPropertyDescriptor(currentOwner, key);
if (descriptor) return {
owner: currentOwner,
descriptor
};
currentOwner = Object.getPrototypeOf(currentOwner);
}
}
//#endregion
//#region src/interceptors/net/utils/normalize-net-connect-args.ts
/**
* Normalizes the arguments passed to `net.connect()`.
*/
function normalizeNetConnectArgs(args) {
if (args.length === 0) return [{ path: "" }, null];
const callback = typeof args[1] === "function" ? args[1] : args[2] || null;
if (typeof args[0] === "string") return [{ path: args[0] }, callback];
if (typeof args[0] === "number") return [{
port: args[0],
path: "",
/**
* @note The host is optional in the "(port[, host][, callback])"
* signatures and defaults to "localhost" in Node.js.
*/
host: typeof args[1] === "string" ? args[1] : void 0
}, callback];
if (typeof args[0] === "object") {
/**
* @note URL arguments receive no special treatment on purpose.
* Node.js does not support them and reads a given URL as a plain
* options object (e.g. "url.port" is a string, "url.host" includes
* the port). The "port" branch below reproduces that reading.
*/
if ("port" in args[0]) return [{
path: "",
port: Reflect.get(args[0], "port"),
host: Reflect.get(args[0], "host"),
auth: Reflect.get(args[0], "auth"),
family: Reflect.get(args[0], "family"),
hints: Reflect.get(args[0], "hints"),
session: Reflect.get(args[0], "session"),
localAddress: Reflect.get(args[0], "localAddress"),
localPort: Reflect.get(args[0], "localPort"),
timeout: Reflect.get(args[0], "timeout"),
lookup: Reflect.get(args[0], "lookup"),
allowHalfOpen: Reflect.get(args[0], "allowHalfOpen"),
noDelay: Reflect.get(args[0], "noDelay"),
keepAlive: Reflect.get(args[0], "keepAlive"),
keepAliveInitialDelay: Reflect.get(args[0], "keepAliveInitialDelay"),
autoSelectFamily: Reflect.get(args[0], "autoSelectFamily"),
autoSelectFamilyAttemptTimeout: Reflect.get(args[0], "autoSelectFamilyAttemptTimeout")
}, callback];
return [{
path: args[0].path || "",
family: Reflect.get(args[0], "family"),
session: Reflect.get(args[0], "session"),
auth: Reflect.get(args[0], "auth"),
timeout: Reflect.get(args[0], "timeout"),
allowHalfOpen: Reflect.get(args[0], "allowHalfOpen")
}, callback];
}
throw new Error(`Invalid arguments passed to net.connect: ${args}`);
}
//#endregion
//#region src/interceptors/net/utils/flush-writes.ts
/**
* Write the given pending data to the socket using its public
* "write()" method. Unlike direct "_writeGeneric()" calls, public
* writes go through the "Writable" queue that only dispatches the
* next write once the previous one completes. TLS sockets rely on
* that serialization: their "TLSWrap" handle supports a single
* write in flight and crashes the process with a native
* "Assertion failed: !current_write_" abort when writes overlap
* (e.g. multiple direct "_writeGeneric()" calls issued on a
* connecting socket replay back-to-back on "connect").
*/
function writePendingData(socket, data, encoding, callback) {
if (Array.isArray(data)) {
for (let index = 0; index < data.length; index++) {
const entry = data[index];
/**
* @note Per-entry callbacks are intentionally ignored, the same
* way "writevGeneric()" only honors the batch-level callback.
*/
if (index === data.length - 1) socket.write(entry.chunk, entry.encoding, callback);
else socket.write(entry.chunk, entry.encoding);
}
return;
}
socket.write(data, encoding, callback);
}
function unwrapPendingData(data, callback) {
if (Array.isArray(data)) for (const entry of data) callback(entry.chunk, entry.encoding, entry.callback);
else callback(data);
}
//#endregion
//#region src/interceptors/net/utils/get-tls-connect-options.ts
/**
* Returns the original options the given TLS socket was created with.
* The original `tls.connect()` stores them on the socket instance
* under an internal symbol before connecting its transport.
* @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1690
*/
function getTlsConnectOptions(socket) {
const kConnectOptions = Object.getOwnPropertySymbols(socket).find((symbol) => {
return symbol.description === "connect-options";
});
if (kConnectOptions == null) return;
return Reflect.get(socket, kConnectOptions);
}
//#endregion
//#region src/interceptors/net/utils/address-info.ts
function getAddressInfoByConnectionOptions(options) {
if (options == null) return {};
const isIPv6 = options.family === 6 || net.isIPv6(options.host || "");
return {
address: isIPv6 ? "::1" : "127.0.0.1",
/**
* @note Coerce the port to a number. Connection options may
* describe it as a string (e.g. when created from a URL), while
* the address info always reports a numeric port.
*/
port: Number(options.port) || (options.protocol === "https:" ? 443 : 80),
family: isIPv6 ? "IPv6" : "IPv4"
};
}
/**
* Get the local address info for the given connection options.
* This describes the client-side end of the connection: the socket
* is bound to the loopback interface and an ephemeral port, the same
* way the operating system binds an outgoing connection.
*/
function getLocalAddressInfoByConnectionOptions(options) {
if (options == null) return {};
const isIPv6 = options.family === 6 || net.isIPv6(options.host || "");
return {
address: options.localAddress || (isIPv6 ? "::1" : "127.0.0.1"),
port: options.localPort || getEphemeralPort(),
family: isIPv6 ? "IPv6" : "IPv4"
};
}
/**
* Get a random port from the ephemeral range (IANA: 49152-65535),
* the range the operating system draws from when binding an
* outgoing connection.
*/
function getEphemeralPort() {
return 49152 + Math.floor(Math.random() * 16384);
}
//#endregion
//#region src/interceptors/net/socket-controller.ts
const kListenerWrap = Symbol("kListenerWrap");
const kRawSocket = Symbol("kRawSocket");
const kPatched = Symbol("kPatched");
const logger$1 = createLogger("socket");
function toServerSocket(socket) {
/**
* The server-side write buffer. Pushing data to the client honors
* the client's read backpressure: once "socket.push()" reports a
* full buffer, subsequent server writes queue here and flush when
* the client reads again ("_read"). This mirrors how a real server
* cannot write faster than the client consumes.
*/
const pendingWrites = [];
let pendingEnd;
let isBackpressured = false;
let isFlushScheduled = false;
const flushPendingWrites = () => {
/**
* @note The mocked connection is not established yet. Flush once
* it is, the same way a real server cannot write to a client
* that has not connected.
*/
if (socket.connecting) {
isBackpressured = true;
if (!isFlushScheduled) {
isFlushScheduled = true;
socket.once("ready", () => {
isFlushScheduled = false;
flushPendingWrites();
});
}
return false;
}
while (pendingWrites.length > 0) {
const nextWrite = pendingWrites.shift();
socket._unrefTimer();
const canPushMore = socket.push(toBuffer(nextWrite.chunk, nextWrite.encoding), nextWrite.encoding);
nextWrite.callback?.();
if (!canPushMore) {
isBackpressured = true;
return false;
}
}
const wasBackpressured = isBackpressured;
isBackpressured = false;
if (pendingEnd) {
const finalEnd = pendingEnd;
pendingEnd = void 0;
if (finalEnd.chunk != null) socket.push(toBuffer(finalEnd.chunk, finalEnd.encoding), finalEnd.encoding);
socket.push(null);
finalEnd.callback?.();
}
if (wasBackpressured) socket.emit("internal:drain");
return true;
};
/**
* @note "_read" is the client asking for more data. Flush the
* buffered server writes so the delivery resumes as the client
* reads, completing the backpressure loop.
*/
const realRead = socket._read.bind(socket);
socket._read = (size) => {
realRead(size);
if (pendingWrites.length > 0 || pendingEnd != null || isBackpressured) flushPendingWrites();
};
return new Proxy(socket, { get: (target, property, receiver) => {
const getRealValue = () => {
return Reflect.get(target, property, receiver);
};
if (property === "on" || property === "addListener" || property === "once" || property === "prependListener" || property === "prependOnceListener") {
const realAddListener = getRealValue();
return (event, listener) => {
if (event === "data") {
const listenerWrap = (chunk, encoding) => {
listener(toBuffer(chunk, encoding));
};
Object.defineProperty(listener, kListenerWrap, {
enumerable: false,
writable: false,
value: listenerWrap
});
/**
* @note Subscribe using the same method (e.g. "once") so its
* listener semantics apply to the internal channel too.
*/
Reflect.apply(realAddListener, target, ["internal:write", listenerWrap]);
return target;
}
/**
* @note The "drain" event on the server socket signals the
* flush of the server-side write buffer. It is routed through
* an internal channel so it does not clash with the "drain"
* event of the underlying client socket.
*/
if (event === "drain") {
Reflect.apply(realAddListener, target, ["internal:drain", listener]);
return target;
}
return realAddListener.call(target, event, listener);
};
}
if (property === "off" || property === "removeListener") {
const realRemoveListener = getRealValue();
return (event, listener) => {
if (event === "data") {
const listenerWrap = listener[kListenerWrap];
if (listenerWrap) return realRemoveListener.call(target, "internal:write", listenerWrap);
}
if (event === "drain") return realRemoveListener.call(target, "internal:drain", listener);
return realRemoveListener.call(target, event, listener);
};
}
if (property === "write") return ((chunk, encoding, callback) => {
if (typeof encoding === "function") {
callback = encoding;
encoding = void 0;
}
pendingWrites.push({
chunk,
encoding,
callback
});
/**
* @note Do not push more data to a client that is not
* reading. The write stays buffered until the client reads
* again, and "drain" signals the flush.
*/
if (isBackpressured) return false;
return flushPendingWrites();
});
if (property === "end") return ((...args) => {
const callback = args[args.length - 1];
/**
* @note The end-of-stream is delivered once the buffered
* writes flush so the client never observes the EOF before
* the data that preceded it.
*/
pendingEnd = {
chunk: typeof args[0] === "function" ? void 0 : args[0],
encoding: typeof args[1] === "string" ? args[1] : void 0,
callback: typeof callback === "function" ? callback : void 0
};
flushPendingWrites();
/**
* @note Do not end the client socket's writable side.
* The server ending the connection only signals EOF to the
* client (the client may keep writing on half-open sockets).
*/
return target;
});
return getRealValue();
} });
}
var SocketController = class SocketController {
static {
this.PENDING = 0;
}
static {
this.CLAIMED = 1;
}
static {
this.PASSTHROUGH = 2;
}
#awaitedVerdicts = 0;
constructor(socket) {
this[kRawSocket] = socket;
socket[kPatched] = true;
this.readyState = SocketController.PENDING;
}
/**
* Claim this socket. Once claimed, the connection attempt succeeds
* regardless of the requested host and the interceptor becomes the
* mocked server for this connection.
*/
claim() {
invariant(this.readyState === SocketController.PENDING, "Failed to claim a socket connection: already handled (%s)", this.readyState);
this.readyState = SocketController.CLAIMED;
}
/**
* Establish this socket connection as-is.
*/
passthrough() {
invariant(this.readyState === SocketController.PENDING, "Failed to passthrough a socket connection: already handled (%s)", this.readyState);
this.readyState = SocketController.PASSTHROUGH;
}
/**
* Await a verdict on this connection from the given number of
* subscribers. A connection nobody awaits to inspect (or one that
* every awaited subscriber has declined) is passed through as-is.
* This makes "unclaimed after everyone declined" a state owned by
* the controller instead of the individual subscribers.
*/
awaitVerdicts(count) {
this.#awaitedVerdicts = count;
if (this.#awaitedVerdicts === 0) this.passthrough();
}
/**
* Decline this socket connection. Declining means the subscriber
* has inspected the connection and will not handle it (e.g. the
* traffic is not of the protocol that subscriber implements).
* Once every awaited subscriber declines, the connection is
* passed through as-is.
*/
decline() {
if (this.readyState !== SocketController.PENDING) return;
this.#awaitedVerdicts -= 1;
if (this.#awaitedVerdicts <= 0)
/**
* @note Defer the passthrough so it never transitions this
* controller in the middle of a client write. Declines are
* issued while the written data is being pushed to the server
* socket, and a synchronous transition would race the write's
* own bookkeeping (e.g. re-buffering the write after a reset
* at an exchange boundary).
*/
process.nextTick(() => {
if (this.readyState === SocketController.PENDING && !this[kRawSocket].destroyed) this.passthrough();
});
}
};
var TcpSocketController = class extends SocketController {
#connectionOptions;
#retargetedConnectionOptions;
#realWriteGeneric;
#passthroughSocket = null;
#bufferedWrites = [];
#readsCorked = false;
#corkedReads = [];
#realHandleSwapped = false;
#resetScheduled = false;
#clientCloseEmitted = false;
#clientEndPushed = false;
#connectEmulated = false;
constructor(socket, createConnection, connectionOptions) {
super(socket);
this.socket = socket;
this.createConnection = createConnection;
/**
* @note Plain TCP sockets capture the connection options from the
* "socket.connect()" proxy below. TLS sockets carry additional
* TLS-level options that never pass through "socket.connect()",
* so those are provided explicitly (see "TlsSocketController").
*/
this.#connectionOptions = connectionOptions;
this.socket._read = () => {
/**
* @note Resume the passthrough socket when the consumer asks for
* more data. Node.js calls "_read()" once the consumer drains the
* read buffer (e.g. after "resume()"). The passthrough socket may
* have been paused when the consumer's buffer got full, and this
* is the only signal to continue reading.
*/
this.#passthroughSocket?.resume();
};
this.#realWriteGeneric = this.socket._writeGeneric;
this.#bufferedWrites = [];
this.socket._writeGeneric = (...args) => {
this.#handleWrite(args);
};
this.socket.connect = new Proxy(this.socket.connect, { apply: (target, thisArg, args) => {
logger$1.verbose("socket.connect() %o", args);
this.#connectionOptions = args[0];
/**
* @note Do not bind the intercepted socket to the requested
* local address/port. Binding reserves the port for real, and
* the passthrough connection (created with the original options)
* would then bind the same port again, resulting in a conflict.
* The requested values are still reflected in the address info
* of a claimed socket (see "claim()").
*/
if (args[0] != null && typeof args[0] === "object" && (args[0].localAddress != null || args[0].localPort != null)) args[0] = {
...args[0],
localAddress: void 0,
localPort: void 0
};
return Reflect.apply(target, thisArg, args);
} });
/**
* @note A single socket can be reused for connections to the same host.
* When one connection ends, the Agent frees the socket, then uses it
* to write the next request's HTTP message immediately. Use the "free"
* event to transition the controller into the pending state so the
* next exchange is handled anew.
*/
socket.on("free", () => {
logger$1.verbose("client socket freed!");
this.reset();
}).on("close", () => {
logger$1.verbose("client socket closed!");
this.#clientCloseEmitted = true;
/**
* @note Destroy the passthrough socket, if any. The client
* socket is done, and a passthrough connection left open would
* linger until the server closes it (or error unhandled if it
* is still connecting).
*/
this.#passthroughSocket?.destroy();
this.#passthroughSocket = null;
this.#bufferedWrites = [];
this.#readsCorked = false;
this.#corkedReads = [];
});
this.serverSocket = toServerSocket(this.socket);
this.pendingConnection = Promise.withResolvers();
this.#reset();
}
/**
* Reset this controller to the pending state so the next exchange
* on this socket can be handled anew. This is meant for kept-alive
* sockets that are reused for multiple exchanges by clients that
* don't emit the "free" event on the socket (e.g. Undici).
*
* Providing connection options retargets this connection: the
* exchanges that follow belong to the given target (e.g. the
* authority of an established "CONNECT" tunnel). An unclaimed
* exchange then passes through to that target instead of the
* originally dialed one, and a claimed exchange reports it as the
* peer. The verdict count is deliberately not re-armed: subscribers
* that declined this connection's protocol stay declined across
* the exchanges, retargeted or not.
*/
reset(connectionOptions) {
if (connectionOptions != null) {
this.#retargetedConnectionOptions = connectionOptions;
this.#connectionOptions = connectionOptions;
/**
* @note The passthrough connection to the original target, if
* any, cannot serve the retargeted exchanges. Detach its
* forwarding listeners before destroying it so its teardown is
* not mistaken for the client connection's own (e.g. its "close"
* must not close the client socket).
*/
if (this.#passthroughSocket) {
this.#passthroughSocket.removeListener("connect", this.#onRealSocketConnect).removeListener("connectionAttemptFailed", this.#onRealSocketConnectionAttemptFailed).removeListener("connectionAttemptTimeout", this.#onRealSocketConnectionAttemptTimeout).removeListener("data", this.#onRealSocketData).removeListener("error", this.#onRealSocketError).removeListener("end", this.#onRealSocketEnd).removeListener("close", this.#onRealSocketClose).destroy();
this.#passthroughSocket = null;
/**
* @note The handle swapped in from the destroyed connection,
* if any, no longer carries this socket's traffic. Treat the
* socket as not swapped so the retargeted passthrough writes
* directly to the new connection until its own handle swap.
*/
this.#realHandleSwapped = false;
}
}
/**
* @note Only settled (claimed or passed-through) sockets need a reset.
* Resetting a pending socket again would discard the writes already
* buffered for the next exchange (e.g. the Agent "free" event firing
* after the parser has reset this controller at a message boundary).
*/
if (this.readyState === SocketController.PENDING) return;
this.#reset();
}
/**
* Schedule a reset of this controller to happen right before the
* next client write. The consumer signals the boundary of the
* current exchange (e.g. an HTTP message boundary) the moment it is
* parsed — mid-delivery of the exchange's final chunk — when an
* immediate reset would discard that chunk. Deferring the reset to
* the next write lets the current exchange settle as-is while the
* next exchange's first write already buffers for a new verdict
* (instead of following the previous one, e.g. leaking to the
* passthrough connection).
*/
scheduleReset() {
this.#resetScheduled = true;
}
#reset() {
logger$1.verbose("resetting the socket...");
this.#resetScheduled = false;
this.readyState = SocketController.PENDING;
this.pendingConnection = Promise.withResolvers();
this.#bufferedWrites = [];
this.#connectEmulated = false;
this.socket._pendingData = null;
this.socket._pendingEncoding = "";
const wrapHandle = (handle) => {
this.pendingConnection.promise.then(() => {
logger$1.verbose("connection request resolved!", this.readyState);
process.nextTick(() => {
/**
* @note If by this point the socket hasn't been handled,
* is still connecting, doesn't have any writes buffered,
* and has a "connect" listener, assume it's the "write after connect"
* scenario (e.g. undici). In that case, auto-claim the socket to
* transition to the connected state appropriately to its handle.
*/
if (this.readyState === SocketController.PENDING && this.socket.connecting && this.#bufferedWrites.length === 0 && this.socket.listenerCount("connect") > 0) {
logger$1.verbose("assume connect->write socket, calling \"connect\" listeners...");
this.emulateConnect();
}
});
});
/**
* Remove the "setTypeOfService" from the handle, if present (Node.js v24+).
* Removing it has no effect on the socket but prevents the "setTypeOfService EBADF" error.
* @see https://github.com/nodejs/node/blob/69a970f76814d40f55cf162d0cc3632fe8a7e599/lib/net.js#L661
* @see https://github.com/nodejs/undici/blob/bf684f7de01616708a33a5d1c092177622394442/lib/dispatcher/client-h1.js#L1136
*/
if (handle.setTypeOfService) handle.setTypeOfService = void 0;
handle.connect = handle.connect6 = (request) => {
logger$1.verbose("handle.connect()");
this.pendingConnection.resolve([request, handle]);
};
logger$1.verbose("socket handle wrapped! waiting for connection request...");
};
if (this.socket._handle) wrapHandle(this.socket._handle);
else this.socket.prependOnceListener("connectionAttempt", () => {
wrapHandle(this.socket._handle);
});
}
/**
* Handle a write performed on the client socket. Installed once as
* "_writeGeneric", this is the single write path for every controller
* state, dispatching on "readyState".
*/
#handleWrite(args) {
const data = args[1];
logger$1.verbose("socket write (state: %d) %o", this.readyState, args);
/**
* @note A scheduled reset marks the boundary of the previous
* exchange (see "scheduleReset"). This write is the first one past
* the boundary; reset before dispatching so it opens the next
* exchange instead of following the previous exchange's verdict.
*/
if (this.#resetScheduled) this.reset();
/**
* @note Buffer the write BEFORE pushing the data to the server socket.
* Handling the pushed data may transition this controller within the
* same call stack, and each transition settles the buffered entry:
* "passthrough()" flushes it to the real socket, "claim()" drops it,
* and a reset at an HTTP message boundary keeps it for the next exchange.
*/
this.#bufferedWrites.push(args);
if (this.readyState === SocketController.PENDING) {
/**
* @note Reflect the buffered write in "_pendingData" so it counts
* toward "bytesWritten", the same way Node.js counts the pending
* data of a connecting socket. Flushing the buffered writes resets
* "_pendingData" (see `passthrough()`).
*/
const pendingData = Array.isArray(this.socket._pendingData) ? this.socket._pendingData : [];
unwrapPendingData(data, (chunk, chunkEncoding) => {
pendingData.push({
chunk,
encoding: chunkEncoding
});
});
this.socket._pendingData = pendingData;
if (this.socket.listenerCount("internal:write") === 0) {
logger$1.verbose("no server data listeners, scheduling to the next tick...");
process.nextTick(() => {
this.#push(data);
});
} else this.#push(data);
} else this.#push(data);
/**
* Dispatch on the state observed AFTER the push since handling the
* pushed data may have transitioned this controller synchronously.
*/
switch (this.readyState) {
case SocketController.PENDING:
/**
* @note The write either awaits the verdict on this exchange or,
* if the push reset this controller at a message boundary, opens
* the next exchange (the reset cleared the buffer; re-buffer it).
*/
if (!this.#bufferedWrites.includes(args)) this.#bufferedWrites.push(args);
this.#acknowledgeWrite(args);
return;
case SocketController.CLAIMED:
/**
* @note Once claimed, there's nowhere else to write chunks to.
* The data was delivered to the server socket; complete the write
* so the socket's writable state settles (enabling "finish").
*/
this.#removeBufferedWrite(args);
this.#acknowledgeWrite(args);
return;
case SocketController.PASSTHROUGH:
/**
* @note A synchronous "passthrough()" has already flushed this
* write (with its callback) to the real socket.
*/
if (!this.#removeBufferedWrite(args)) return;
/**
* @note Until the handle swap, the client socket's own handle
* cannot carry any data (it never actually connects). Write
* directly to the passthrough socket instead. This also prevents
* the "connecting" write replay of `Socket.prototype._writeGeneric`
* from pushing the same data to the server socket twice.
*/
if (!this.#realHandleSwapped && this.#passthroughSocket) {
writePendingData(this.#passthroughSocket, data, args[2], args[3]);
return;
}
this.#realWriteGeneric.apply(this.socket, args);
}
}
/**
* Complete the given write by invoking its callback. The callback is
* then removed from the write entry so flushing that entry later
* (e.g. on passthrough) does not invoke it twice.
*/
#acknowledgeWrite(args) {
const callback = args[3];
if (typeof callback === "function") {
callback();
args[3] = void 0;
}
}
#removeBufferedWrite(args) {
const index = this.#bufferedWrites.indexOf(args);
if (index === -1) return false;
this.#bufferedWrites.splice(index, 1);
return true;
}
emulateConnect() {
this.#connectEmulated = true;
/**
* @note Reflect the connected state before notifying the listeners,
* the same way Node.js does before emitting "connect".
*/
Reflect.set(this.socket, "connecting", false);
/**
* @note Invoke the raw listeners so the "once" wrappers get
* consumed. This prevents listeners like the connection callback
* from being invoked again when "connect" is emitted for real
* (e.g. once the claimed connection completes).
*/
for (const listener of this.socket.rawListeners("connect")) listener.apply(this.socket);
}
/**
* Push the given data to the server socket.
* This has no effect on the public-facing socket and is used
* only for the interceptors to subscribe to "socket.on('data')"
* before the data is actually written anywhere.
*/
#push = (data) => {
if (data == null) return;
logger$1.verbose("server push %o", data);
unwrapPendingData(data, (chunk, encoding) => {
logger$1.verbose("server emitting \"data\" %o", {
chunk,
encoding
});
this.socket.emit("internal:write", chunk, encoding);
});
};
#onRealSocketConnect = () => {
if (!this.#passthroughSocket) return;
/**
* @note The consumer may destroy the socket while the passthrough
* connection is still being established. The passthrough "connect"
* (I/O poll phase) can arrive before the client's "close" callback
* (close phase) within the same event loop iteration. A destroyed
* socket must not emit "connect"/"ready".
*/
if (this.socket.destroyed) {
this.#passthroughSocket.destroy();
return;
}
const replacedHandle = this.socket._handle;
const wasUnrefed = replacedHandle != null && typeof replacedHandle.hasRef === "function" && !replacedHandle.hasRef();
this.socket._handle = this.#passthroughSocket._handle;
this.#realHandleSwapped = true;
/**
* @note Preserve the ref state across the handle swap. If the
* consumer unrefed the socket while it was connecting, the swapped
* handle must not hold the process alive either.
*/
if (wasUnrefed) this.socket._handle.unref?.();
/**
* @note Close the replaced handle. Nothing references it past this
* point, and left open, it keeps the process alive indefinitely.
* For TLS sockets, the replaced handle is a TLSWrap; close its
* underlying transport too (closing the wrap alone does not close
* the TCP handle it sits on).
*/
if (replacedHandle != null) {
replacedHandle.close();
replacedHandle._parent?.close();
}
Reflect.set(this.socket, "connecting", false);
/**
* @note Read the remote address info once so it gets cached on the
* socket. The passthrough socket controls the swapped handle and may
* close it at any point (e.g. once the server ends the connection),
* while Node.js keeps serving the cached info for sockets that
* have connected. Reading must happen after the socket is no longer
* connecting (the info of connecting sockets is never cached).
*/
this.socket.remoteAddress;
this.socket.emit("connect");
this.socket.emit("ready");
};
#onRealSocketConnectionAttemptFailed = (address, port, family, error) => {
this.socket.emit("connectionAttemptFailed", address, port, family, error);
};
#onRealSocketConnectionAttemptTimeout = (address, port, family) => {
this.socket.emit("connectionAttemptTimeout", address, port, family);
};
#onRealSocketData = (data) => {
logger$1.verbose("real socket \"data\" event %o", data);
/**
* @note Receiving data is socket activity. Refresh the idle timer
* of the client socket the same way its own reads would
* ("socket.setTimeout()" must not fire during an active transfer).
* The data arrives on the real socket, which only refreshes the
* real socket's timer.
*/
this.socket._unrefTimer();
if (this.#readsCorked) {
logger$1.verbose("reads are corked, buffering the data...");
this.#corkedReads.push({
type: "data",
chunk: data
});
return;
}
if (!this.socket.push(data)) {
logger$1.verbose("client socket forbade more pushes, pausing the passthrough socket...");
this.#passthroughSocket?.pause();
}
};
#onRealSocketError = (error) => {
logger$1.verbose("real socket \"error\" event %o", error);
if (this.socket.destroyed) {
logger$1.verbose("real socket errored but client socket already destroyed, skipping...");
return;
}
logger$1.verbose("real socket errored, forwarding %o", error);
this.socket.destroy(error);
if (this.#realHandleSwapped) process.nextTick(() => this.socket.emit("close", true));
};
#onRealSocketEnd = () => {
this.socket._unrefTimer();
if (this.#readsCorked) {
this.#corkedReads.push({ type: "end" });
return;
}
this.#clientEndPushed = true;
this.socket.push(null);
};
#onRealSocketClose = (hadError) => {
/**
* @note The connection is fully closed and the swapped handle
* cannot shut down anymore. Report a synchronous shutdown so the
* automatic "end()" of a half-closed socket ("allowHalfOpen: false")
* finishes without errors once the consumer reads the end-of-stream.
*/
if (this.#realHandleSwapped && this.socket._handle) this.socket._handle.shutdown = () => 1;
if (this.#readsCorked) {
this.#corkedReads.push({
type: "close",
hadError
});
return;
}
if (this.#clientCloseEmitted) return;
if (this.socket.destroyed && !this.#realHandleSwapped) return;
this.#emitClientClose(hadError);
};
/**
* Emit the "close" event on the client socket, honoring the order
* of the socket teardown events.
* @note The client may be paused with the received data (and the
* end-of-stream) still buffered. Node.js never emits "close" before
* "end" on a gracefully closed connection: the teardown waits until
* the consumer reads the buffered data.
*/
#emitClientClose(hadError) {
if (this.#clientEndPushed && !this.socket.readableEnded && !this.socket.destroyed) {
let closeDelivered = false;
const deliverClose = (hadErrorOverride) => {
if (closeDelivered) return;
closeDelivered = true;
process.nextTick(() => {
if (!this.#clientCloseEmitted) this.socket.emit("close", hadErrorOverride ?? hadError);
});
};
this.socket.once("end", () => {
deliverClose();
});
/**
* @note The consumer may also destroy the socket before reading
* the buffered data. Node.js still emits "close" for such
* sockets, but the destroy machinery of a socket with a swapped
* (already closed) handle never completes. Deliver "close" here.
*/
const realDestroy = this.socket._destroy;
this.socket._destroy = (error, callback) => {
deliverClose(error != null);
return realDestroy.call(this.socket, error, callback);
};
return;
}
this.socket.emit("close", hadError);
}
#onMockSocketDrain = () => {
logger$1.verbose("client socket drained!");
this.#passthroughSocket?.resume();
};
/**
* Forward the client's half-close to the passthrough socket.
* The client's writable side may finish before the real handle is
* swapped in ("_final" of a connected client runs against the mock
* handle), leaving the FIN unsent. Once the handle is swapped, the
* client's own shutdown reaches the real connection natively.
*/
#forwardClientFinish = () => {
if (!this.#realHandleSwapped) this.#passthroughSocket?.end();
};
/**
* Suspend forwarding of the passthrough socket events ("data", "end", "close")
* to the client socket. The events are buffered in order until `uncorkReads()`
* is called. This allows the consumer to delay the delivery of the original
* response to the client (e.g. until its own asynchronous logic settles).
*
* @note Pausing the client socket is not enough to delay the delivery.
* Consumers like Undici read the pushed data from a paused socket
* directly via `socket.read()`, which is unaffected by `socket.pause()`.
*/
corkReads() {
this.#readsCorked = true;
}
/**
* Resume forwarding of the passthrough socket events to the client socket,
* replaying any events buffered while the reads were corked.
*/
uncorkReads() {
if (!this.#readsCorked) return;
this.#readsCorked = false;
for (const corkedRead of this.#corkedReads.splice(0)) switch (corkedRead.type) {
case "data":
if (!this.socket.push(corkedRead.chunk)) {
logger$1.verbose("client socket forbade more pushes, pausing the passthrough socket...");
this.#passthroughSocket?.pause();
}
break;
case "end":
this.#clientEndPushed = true;
this.socket.push(null);
break;
case "close":
this.#emitClientClose(corkedRead.hadError);
break;
}
}
claim() {
super.claim();
/**
* @note The client may destroy the socket before the connection
* is claimed (e.g. abort a request in-flight). There is no
* connection to mock then, and the destroyed socket has no handle.
*/
if (this.socket.destroyed) {
logger$1.verbose("socket already destroyed, skipping claim...");
return;
}
/**
* @note Skip already connected sockets (e.g. kept-alive sockets
* reused for the next exchange). Sockets with an emulated "connect"
* only appear connected and must still complete the mock connection.
*/
if (!this.socket.connecting && !this.#connectEmulated) {
logger$1.verbose("socket already connected, skipping claim...");
return;
}
logger$1.verbose("-> claim!");
/**
* @note Reflect the local end of the claimed socket, the same way
* the operating system reports the bound address of an outgoing
* connection via "socket.address()"/"localAddress"/"localPort".
* Patching the handle also prevents Node.js from handling the
* "getsockname" errors of the never-connected raw handle.
* @see https://github.com/nodejs/node/blob/13eb80f3b718452213e0fc449702aefbbfe4110f/lib/net.js#L971
*/
this.socket._handle.getsockname = (addressInfo) => {
Object.assign(addressInfo, getLocalAddressInfoByConnectionOptions(this.#connectionOptions));
return 0;
};
/**
* @note Reflect the connection target as the peer of the claimed
* socket, the same way a connected socket reports the server it
* connected to via "remoteAddress"/"remotePort"/"remoteFamily".
*/
this.socket._handle.getpeername = (addressInfo) => {
Object.assign(addressInfo, getAddressInfoByConnectionOptions(this.#connectionOptions));
return 0;
};
this.#bufferedWrites = [];
this.socket._pendingData = null;
this.socket._pendingEncoding = "";
this.pendingConnection.promise.then(([request, handle]) => {
logger$1.verbose("connection request resolved, mocking the connection...");
/**
* @note "afterConnect" asserts that the socket is connecting.
* Restore the flag if the connect was emulated earlier (emulation
* flips it so its listeners observe a connected socket).
* "afterConnect" itself sets it back to false.
*/
if (this.#connectEmulated) Reflect.set(this.socket, "connecting", true);
/**
* @see https://github.com/nodejs/node/blob/9cd6630870b776e96c5cf0ac68c31e2f46df3835/lib/net.js#L1142
*/
request.oncomplete(0, handle, request, true, true);
});
}
passthrough(flushPendingData) {
super.passthrough();
logger$1.verbose("-> passthrough!");
const createRealSocket = () => {
const realSocket = this.#retargetedConnectionOptions ? this.#createRetargetedConnection(this.#retargetedConnectionOptions) : this.createConnection();
realSocket[kPatched] = true;
if (this.socket.timeout != null) realSocket.setTimeout(this.socket.timeout);
return realSocket;
};
const realSocket = this.#passthroughSocket && !this.#passthroughSocket.destroyed ? this.#passthroughSocket : createRealSocket();
if (realSocket !== this.#passthroughSocket) this.#passthroughSocket = realSocket;
if (this.#bufferedWrites.length === 0) logger$1.verbose("passthrough with empty writes buffer (state: %d)", this.readyState);
/**
* Flush any writes during the pending phase to the passthrough socket.
* @note These are written directly on the passthrough socket to prevent
* them from being forwarded as "data" events on the server (already emitted).
*/
for (let i = 0; i < this.#bufferedWrites.length; i++) {
const pendingWrite = this.#bufferedWrites[i];
if (i === 0 && typeof flushPendingData === "function") {
const data = pendingWrite[1];
const encoding = pendingWrite[2];
flushPendingData(data, encoding, (nextData) => {
pendingWrite[1] = nextData;
});
}
const [, data, encoding, callback] = pendingWrite;
writePendingData(realSocket, data, encoding, callback);
}
this.#bufferedWrites = [];
this.socket._pendingData = null;
this.socket._pendingEncoding = "";
this.socket.address = realSocket.address.bind(realSocket);
this.socket.removeListener("drain", this.#onMockSocketDrain);
this.socket.on("drain", this.#onMockSocketDrain);
realSocket.removeListener("connect", this.#onRealSocketConnect).removeListener("connectionAttemptFailed", this.#onRealSocketConnectionAttemptFailed).removeListener("connectionAttemptTimeout", this.#onRealSocketConnectionAttemptTimeout).removeListener("data", this.#onRealSocketData).removeListener("error", this.#onRealSocketError).removeListener("end", this.#onRealSocketEnd).removeListener("close", this.#onRealSocketClose);
realSocket.once("connect", this.#onRealSocketConnect).on("connectionAttemptFailed", this.#onRealSocketConnectionAttemptFailed).on("connectionAttemptTimeout", this.#onRealSocketConnectionAttemptTimeout).on("data", this.#onRealSocketData).on("error", this.#onRealSocketError).on("end", this.#onRealSocketEnd).on("close", this.#onRealSocketClose);
/**
* @note Forward the client's half-close, unless the real handle
* is already swapped in — the client's own shutdown then reaches
* the real connection natively (see "#forwardClientFinish").
*/
if (!this.#realHandleSwapped) if (this.socket.writableFinished) this.#forwardClientFinish();
else {
this.socket.removeListener("finish", this.#forwardClientFinish);
this.socket.once("finish", this.#forwardClientFinish);
}
return realSocket;
}
/**
* Create the passthrough connection to the target this controller
* was retargeted to (see `reset()`). The original `createConnection`
* dials the originally requested target and cannot serve retargeted
* exchanges.
*/
#createRetargetedConnection(connectionOptions) {
const realSocket = new net.Socket();
/**
* @note Mark the socket as patched before connecting: the patched
* "Socket.prototype.connect" exempts such sockets, establishing
* the connection for real.
*/
realSocket[kPatched] = true;
return realSocket.connect(connectionOptions);
}
};
var TlsSocketController = class extends TcpSocketController {
/**
* @note The TLS connection options must be provided explicitly.
* They cannot be captured from "socket.connect()" like for plain
* TCP sockets because "tls.connect()" fixes them at the TLS socket
* construction, before its transport ever connects.
*/
#tlsConnectionOptions;
constructor(socket, createConnection, tlsConnectionOptions) {
super(socket, createConnection, tlsConnectionOptions);
this.socket = socket;
this.createConnection = createConnection;
this.#tlsConnectionOptions = tlsConnectionOptions;
socket.prependListener("secureConnect", () => {
/**
* @note Reflect the negotiated ALPN protocol from the handle that
* completed the handshake. The socket sets "alpnProtocol" only in
* its own "_finishInit", which never runs for passthrough
* connections (the handshake completes on the passthrough socket
* whose handle this socket inherits).
*/
socket.alpnProtocol = socket._handle.getALPNNegotiatedProtocol();
});
}
emulateConnect() {
super.emulateConnect();
for (const listener of this.socket.rawListeners("secureConnect")) listener.apply(this.socket);
}
claim() {
/**
* @note The client may destroy the socket before the connection
* is claimed. Defer to the parent class, which transitions the
* ready state and skips the mock connection of destroyed sockets.
*/
if (this.socket.destroyed) {
super.claim();
return;
}
/**
* @note Reflect that the mocked connection is not authorized.
* There is no real peer certificate to verify. The identity check
* itself is skipped for claimed connections (see the
* "isSessionReused" mock below).
*/
this.socket.prependOnceListener("secureConnect", () => {
Reflect.set(this.socket, "authorized", false);
Reflect.set(this.socket, "authorizationError", "MOCKED_CONNECTION_NOT_VERIFIED");
});
const handle = this.socket._handle;
handle.start = () => void 0;
/**
* Mock this to prevent the "Error: Worker exited unexpectedly" error.
* This will trigger when "secure" is emitted.
* @see https://github.com/nodejs/node/blob/bdc8131fa78089b81b74dbff467365afb6536e6a/lib/internal/tls/wrap.js#L1648
*/
handle.verifyError = () => void 0;
handle.getSession = () => {
return Buffer.from("mocked session");
};
/**
* @note Skip the server identity check for this mocked connection.
* Node.js runs "options.checkServerIdentity" in "onConnectSecure"
* against the peer certificate, and a mocked connection has no
* real peer certificate — the caller's (or the default) validation
* would fail and destroy the socket. The check is overridden on the
* socket's internal connect options since the emulated handshake
* still completes through the regular "onConnectSecure" path.
* @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1621
*/
const realTlsConnectOptions = getTlsConnectOptions(this.socket);
if (realTlsConnectOptions) realTlsConnectOptions.checkServerIdentity = () => {};
handle.getCipher = () => {
return {
name: "TLS_AES_256_GCM_SHA384",
standardName: "TLS_AES_256_GCM_SHA384",
version: "TLSv1.3"
};
};
/**
* Mock this to prevent a segfault on Node.js 26+. The native
* implementation reads the negotiated group ("SSL_get0_group_name")
* of a handshake that never happened. Node.js itself calls this
* in "onConnectSecure" to validate the "minDHSize" option.
* Reflect the ephemeral key exchange matching the mocked cipher.
* @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1636
*/
handle.getEphemeralKeyInfo = () => {
return {
type: "ECDH",
name: "X25519",
size: 253
};
};
const requestedAlpnProtocols = this.#tlsConnectionOptions?.ALPNProtocols;
if (Array.isArray(requestedAlpnProtocols) && requestedAlpnProtocols.length > 0) {
const [preferredProtocol] = requestedAlpnProtocols;
/**
* @note Reflect the client's preferred ALPN protocol as the
* negotiated one. The mocked server accepts whatever the
* client prefers.
*/
handle.getALPNNegotiatedProtocol = () => {
return typeof preferredProtocol === "string" ? preferredProtocol : false;
};
}
this.socket.once("connect", () => {
/**
* @note A TLS 1.3 handshake derives five secrets, each reported
* via a separate "keylog" event before the handshake completes.
* Reflect them with mocked key material matching the mocked
* cipher (SHA-384 secrets; the format is "LABEL <client random>
* <secret>", each value hex-encoded).
*/
const mockedClientRandom = "0".repeat(64);
const mockedSecret = "0".repeat(96);
for (const keylogLabel of [
"SERVER_HANDSHAKE_TRAFFIC_SECRET",
"EXPORTER_SECRET",
"SERVER_TRAFFIC_SECRET_0",
"CLIENT_HANDSHAKE_TRAFFIC_SECRET",
"CLIENT_TRAFFIC_SECRET_0"
]) this.socket.emit("keylog", Buffer.from(`${keylogLabel} ${mockedClientRandom} ${mockedSecret}\n`));
handle.onhandshakedone();
/**
* @note A TLS 1.3 server issues two session tickets by default,
* each emitting a separate "session" event on the client.
*/
handle.onnewsession(1, Buffer.from("mocked session"));
handle.onnewsession(2, Buffer.from("mocked session"));
});
super.claim();
}
passthrough(flushPendingData) {
const realSocket = super.passthrough(flushPendingData);
/**
* @note Remove the internal "connect" listener added by the TLS socket.
* Normally, that listener manages the SSL handshake. But since we're in passthrough,
* we delegate that to the real socket. Leaving the listener on the mock socket while
* inheriting the real socket's handle will result in the handshake performed twice, which is a no-op.
* @see https://github.com/nodejs/node/blob/abddfc921bf2af02a04a6a5d2bca8e2d91d80958/lib/internal/tls/wrap.js#L1105
*
* This prevents the following error:
* # node (vitest 4)[8686]: static void node::crypto::TLSWrap::Start(const FunctionCallbackInfo<Value> &) at ../src/crypto/crypto_tls.cc:589
# Assertion failed: !wrap->started_
*/
for (const connectListener of this.socket.listeners("connect")) if (connectListener === this.socket._start || "listener" in connectListener && connectListener.listener === this.socket._start) this.socket.removeListener("connect", connectListener);
realSocket.on("secure", () => {
this.socket.emit("secure");
}).on("session", (...args) => {
this.socket.emit("session", ...args);
}).on("keylog", (...args) => {
this.socket.emit("keylog", ...args);
}).on("OCSPResponse", (...args) => {
this.socket.emit("OCSPResponse", ...args);
});
return realSocket;
}
};
//#endregion
//#region src/interceptors/net/utils/normalize-tls-connect-args.ts
function normalizeTlsConnectArgs(args) {
/**
* @note Despite incorrect type definitions, "tls.connect()" has all the
* options of "net.connect()" and then those specific to TLS connections,
* like "session" or "socket".
* @see https://github.com/nodejs/node/blob/bdc8131fa78089b81b74dbff467365afb6536e6a/lib/internal/tls/wrap.js#L1615
*/
const netConnectArgs = normalizeNetConnectArgs(args);
const options = netConnectArgs[0];
const callback = netConnectArgs[1];
if (args[0] !== null && typeof args[0] === "object") Object.assign(options, args[0]);
else if (args[1] !== null && typeof args[1] === "object") Object.assign(options, args[1]);
else if (args[2] !== null && typeof args[2] === "object") Object.assign(options, args[2]);
return callback ? [options, callback] : [options];
}
//#endregion
//#region src/interceptors/net/index.ts
var SocketConnectionEvent = class extends TypedEvent {
constructor(data) {
super(...["connection", {}]);
this.socket = data.socket;
this.connectionOptions = data.connectionOptions;
this.controller = data.controller;
}
};
const logger = createLogger("socket");
/**
* A DNS lookup function for intercepted sockets. It always succeeds,
* resolving any hostname to the loopback address. This ensures the
* "lookup"/"connectionAttempt" socket events fire even for non-existent
* hosts, and no real DNS resolution is performed. Passthrough
* connections are created with the original options and use the real
* (or the caller's custom) lookup instead.
*/
const mockLookup = (hostname, dnsOptions, callback) => {
const family = dnsOptions.family === 6 ? 6 : 4;
const address = family === 6 ? "::1" : "127.0.0.1";
/**
* @note Call back asynchronously since DNS lookup is always
* asynchronous in Node.js. Calling back synchronously emits
* the "lookup"/"connectionAttempt" socket events before the
* consumer gets a chance to add listeners for them.
*/
process.nextTick(() => {
/**
* @note Honor the Node.js lookup contract: the callback receives
* an array of addresses only when the "all" option is set
* (e.g. during the family autoselection). Otherwise, it receives
* a single address and its family. Node.js rejects an array in
* the latter case with "ERR_INVALID_IP_ADDRESS".
*/
if (dnsOptions.all) {
callback(null, [{
address,
family
}]);
return;
}
callback(null, address, family);
});
};
/**
* Interceptor for `net.Socket` connections.
*/
var SocketInterceptor = class extends Interceptor {
static {
this.symbol = Symbol.for("socket-interceptor");
}
predicate() {
return true;
}
setup() {
const interceptor = this;
/**
* @note A synchronous re-entrancy latch for creating passthrough
* TLS connections. The original "tls.connect()" constructs a TLS
* socket and calls "socket.connect()" on it synchronously, and that
* call must reach Node.js as-is instead of being intercepted again.
*/
let isCreatingPassthroughConnection = false;
this.subscriptions.push(
/**
* @note Intercept connections at the "net.Socket.prototype.connect"
* level instead of patching the "net.connect()" module function.
* ESM consumers snapshot the module bindings at import time
* ("import * as net from 'node:net'"), so reassigning "net.connect"
* is invisible to them. Every client connection ends up calling
* "Socket.prototype.connect" (including the one made by the original
* "net.connect()"), and prototype mutations are visible regardless
* of how the module was imported.
*/
patchesRegistry.applyPatch(net.Socket.prototype, "connect", (realSocketConnect) => {
return function connect(...args) {
const socket = this;
/**
* @note Skip the sockets this interceptor already controls
* (the mock connect call below, re-connects of an intercepted
* socket, passthrough sockets) and the sockets created while
* dialing a passthrough TLS connection. Their connects must
* reach Node.js as-is.
*/
if (socket[kPatched] || isCreatingPassthroughConnection) return realSocketConnect.apply(socket, args);
logger.verbose("socket.connect() %o", args);
/**
* @note The original "net.connect()"/"tls.connect()" normalize
* the arguments themselves and call this method with the
* normalized array instead of the individual arguments.
*/
const connectArgs = Array.isArray(args[0]) ? args[0] : args;
const [transportConnectionOptions, connectionCallback] = normalizeNetConnectArgs(connectArgs);
logger.verbose("connection options %o", {
transportConnectionOptions,
connectionCallback
});
let controller;
let connectionOptions;
if (socket instanceof tls.TLSSocket) {
/**
* @note TLS sockets arrive here from the original
* "tls.connect()", which connects the transport of the TLS
* socket it constructs ("connectionCallback" is its internal
* "_start" handshake trigger). The original TLS connection
* options are recovered from the socket instance since the
* construction has already happened.
*/
const realTlsConnectionOptions = getTlsConnectOptions(socket);
const [tlsConnectionOptions] = normalizeTlsConnectArgs([{
...transportConnectionOptions,
...realTlsConnectionOptions
}]);
connectionOptions = tlsConnectionOptions;
controller = new TlsSocketController(socket, () => {
/**
* @note Create the passthrough connection via the original
* "tls.connect()" with the original connection options
* (the real DNS lookup and the caller's certificate
* validation included). The latch exempts the transport
* connect of that connection from interception.
*/
isCreatingPassthroughConnection = true;
try {
return tls.connect(realTlsConnectionOptions ?? tlsConnectionOptions);
} finally {
isCreatingPassthroughConnection = false;
}
}, tlsConnectionOptions);
} else {
/**
* @note Create passthrough connections without the connection
* callback. The callback is already registered as a "connect"
* listener on the consumer's socket. Passing it to the
* passthrough connection would invoke it twice.
*/
const passthroughArgs = connectArgs.filter((arg) => {
return typeof arg !== "function";
});
/**
* @note Create the passthrough socket with the original
* options, the same way "net.connect()" does. Connect it via
* the unpatched method so the passthrough connection is not
* intercepted again.
*/
const socketOptions = connectArgs[0] !== null && typeof connectArgs[0] === "object" && !("href" in connectArgs[0]) ? connectArgs[0] : {};
connectionOptions = transportConnectionOptions;
controller = new TcpSocketController(socket, () => {
const passthroughSocket = new net.Socket(socketOptions);
Reflect.apply(realSocketConnect, passthroughSocket, passthroughArgs);
return passthroughSocket;
});
}
process.nextTick(() => {
if (socket.destroyed) return;
/**
* @note Expect a verdict on this connection from every
* "connection" listener before emitting the event. With no
* listeners to claim the connection (or once every listener
* declines it), the controller passes it through as-is.
*/
controller.awaitVerdicts(interceptor.listenerCount("connection"));
interceptor.emitter.emit(new SocketConnectionEvent({
socket: controller.serverSocket,
controller,
connectionOptions
}));
logger.verbose("emitted \"connection\" event!");
});
logger.verbose("connecting the socket...");
/**
* @note The requested local address/port are stripped from the
* actual "socket.connect()" call by the controller to prevent
* binding the intercepted socket (see the "connect" proxy).
*/
const mockConnectionOptions = { ...transportConnectionOptions };
mockConnectionOptions.lookup = mockLookup;
try {
/**
* @note The normalized options are looser than the declared
* "SocketConnectOpts" (e.g. the port may be a string when a
* URL is passed). Node.js validates them at runtime.
* This call goes through the controller's "connect" proxy
* and lands back in this patch, where the "kPatched" check
* above delegates it to the unpatched method.
*/
return socket.connect(mockConnectionOptions, connectionCallback ?? void 0);
} catch (error) {
/**
* @note "socket.connect()" can throw synchronously on invalid
* input (e.g. a bad port). Destroy the socket so the pending
* interception tick does not act on it, then let the error
* propagate to the consumer like in Node.js.
*/
socket.destroy();
throw error;
}
};
}),
this.#stopReusingUnpatchedSockets()
);
/**
* @note "net.connect()"/"net.createConnection()" need no patching of
* their own: both construct a "net.Socket" and call the patched
* "Socket.prototype.connect" on it.
*/
}
/**
* Prevent the `net.Socket` instances created before this interceptor
* was applied from getting reused by an `Agent`. Purging them from the
* keep-alive pool forces the agent to establish new (intercepted)
* connections instead.
*/
#stopReusingUnpatchedSockets() {
/**
* @note "Agent.prototype.addRequest" is undocumented but stable:
* its signature changed once (the keep-alive Agent rewrite in
* Node.js 0.12), and the ecosystem (agent-base, agentkeepalive,
* APM wrappers) has relied on it ever since. "https.Agent"
* inherits it, so a single patch covers both.
*/
if (typeof http.Agent.prototype.addRequest !== "function") return () => {};
return patchesRegistry.applyPatch(http.Agent.prototype, "addRequest", (realAddRequest) => {
return function(...args) {
/**
* @note Destroy the free sockets created before the interceptor
* was applied. Destroying flips their "destroyed" state
* synchronously, so the original "addRequest" below discards
* them and dials a new (intercepted) connection instead.
* The "freeSockets" pool only contains idle sockets by
* definition, so destroying them aborts nothing in-flight.
*/
for (const sockets of Object.values(this.freeSockets)) {
if (sockets == null) continue;
for (const socket of sockets) if (!socket[kPatched]) socket.destroy();
}
return realAddRequest?.apply(this, args);
};
});
}
};
//#endregion
export { getDeepPropertyDescriptor as a, unwrapPendingData as i, SocketController as n, patchesRegistry as o, kRawSocket as r, SocketInterceptor as t };
//# sourceMappingURL=net-Ca8p1rIe.js.map

Sorry, the diff of this file is too big to display

import { a as createLogger, i as Interceptor, o as formatRequest, r as toBuffer } from "./buffer-utils-BvPY1Tc-.js";
import { a as isResponseError, c as isObject, d as createRequestId, f as RequestController, l as getRawFetchHeaders, n as FetchResponse, o as isResponseLike, p as InterceptorError, r as createServerErrorResponse, s as kErrorResponse, t as FetchRequest, u as recordRawFetchHeaders } from "./fetch-utils-Dw1PsmtX.js";
import { i as unwrapPendingData, n as SocketController, r as kRawSocket, t as SocketInterceptor } from "./net-Ca8p1rIe.js";
import { TypedEvent } from "rettime";
import { invariant } from "outvariant";
import { IncomingMessage, METHODS, STATUS_CODES, ServerResponse } from "node:http";
import { AsyncLocalStorage } from "node:async_hooks";
import net from "node:net";
import tls from "node:tls";
import { Readable } from "node:stream";
import fs from "node:fs";
import { until } from "@open-draft/until";
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
//#region src/request-context.ts
const requestContext = new AsyncLocalStorage();
function runInRequestContext(callback, logger) {
/**
* @note Never shadow an existing request context. Nested calls
* (e.g. a patched entry point re-entered synchronously, or a request
* made within the fetch/XMLHttpRequest interceptor context) must run
* within the parent context so the sockets they create capture it.
*/
if (requestContext.getStore()) return callback();
/**
* @note The initiator is the callback's return value (e.g. the
* "ClientRequest" instance), so it cannot be known before running
* the callback. The context is mutated in place once the callback
* returns; readers hold the context object by reference and sample
* "initiator" only after the request has been written.
*/
const context = {
initiator: void 0,
logger
};
return requestContext.run(context, () => {
const initiator = callback();
context.initiator = initiator;
return initiator;
});
}
//#endregion
//#region src/interceptors/http/forward-events.ts
const logger = createLogger("http-request");
function forwardHttpEvents(options) {
const controller = new AbortController();
const { source, emitter, predicate, responsePredicate } = options;
source.on("request", async (event) => {
if (predicate(event.initiator)) {
logger.verbose("forwarding \"request\" event %o", { requestId: event.requestId });
await emitter.emitAsPromise(event);
}
}, { signal: controller.signal });
const responseListener = async (event) => {
if (predicate(event.initiator) && (responsePredicate == null || responsePredicate(event))) {
logger.verbose("forwarding \"response\" event %o", {
requestId: event.requestId,
responseType: event.responseType
});
await emitter.emitAsPromise(event);
}
};
const unhandledExceptionListener = async (event) => {
if (predicate(event.initiator)) {
logger.verbose("forwarding \"unhandledException\" event %o", { requestId: event.requestId });
await emitter.emitAsPromise(event);
}
};
const addResponseListener = () => {
if (!source.listeners("response").includes(responseListener)) source.on("response", responseListener, { signal: controller.signal });
};
const addUnhandledExceptionListener = () => {
if (!source.listeners("unhandledException").includes(unhandledExceptionListener)) source.on("unhandledException", unhandledExceptionListener, { signal: controller.signal });
};
if (emitter.listenerCount("response") > 0) addResponseListener();
if (emitter.listenerCount("unhandledException") > 0) addUnhandledExceptionListener();
emitter.hooks.on("newListener", (type) => {
if (type === "response") addResponseListener();
if (type === "unhandledException") addUnhandledExceptionListener();
}, {
signal: controller.signal,
persist: true
});
emitter.hooks.on("removeListener", (type) => {
if (type === "response" && emitter.listenerCount("response") === 0) source.removeListener("response", responseListener);
if (type === "unhandledException" && emitter.listenerCount("unhandledException") === 0) source.removeListener("unhandledException", unhandledExceptionListener);
}, {
signal: controller.signal,
persist: true
});
return () => {
controller.abort();
};
}
//#endregion
//#region src/events/http.ts
var HttpRequestEvent = class extends TypedEvent {
constructor(data) {
super(...["request", {}]);
this.request = data.request;
this.requestId = data.requestId;
this.initiator = data.initiator;
this.controller = data.controller;
}
};
var HttpResponseEvent = class extends TypedEvent {
constructor(data) {
super(...["response", {}]);
this.response = data.response;
this.responseType = data.responseType;
this.request = data.request;
this.requestId = data.requestId;
this.initiator = data.initiator;
}
};
var UnhandledHttpException = class extends TypedEvent {
constructor(data) {
super(...["unhandledException", {}]);
this.error = data.error;
this.request = data.request;
this.requestId = data.requestId;
this.initiator = data.initiator;
this.controller = data.controller;
}
};
//#endregion
//#region src/interceptors/net/utils/connection-options-to-url.ts
/**
* Creates a `URL` instance out of the `net.connect()` options.
* @note This implies that the passed connection is an HTTP connection.
*/
function connectionOptionsToUrl(options, socket) {
const isIPv6 = net.isIPv6(options.host || "");
const protocol = socket instanceof tls.TLSSocket ? "https:" : getProtocolByConnectionOptions(options);
const host = options.host || "localhost";
const url = new URL(`${protocol}//${isIPv6 ? `[${host}]` : host}`);
if (options.path) url.pathname = options.path;
if (options.port) url.port = options.port.toString();
if (options.auth) {
const [username, password] = options.auth.split(":");
/**
* Authentication options are provided as plain values.
* Encode them to form a valid URL.
* @see https://github.com/nodejs/node/blob/f3adc11e37b8bfaaa026ea85c1cf22e3a0e29ae9/lib/internal/url.js#L1452
*/
url.username = encodeURIComponent(username);
url.password = encodeURIComponent(password);
}
return url;
}
function getProtocolByConnectionOptions(options) {
if (options.protocol) return options.protocol;
if (options.port === 443) return "https:";
return "http:";
}
//#endregion
//#region src/interceptors/http/http-parser/index.ts
var import_constants = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports) => {
Object.defineProperty(exports, "__esModule", { value: true });
exports.SPECIAL_HEADERS = exports.MINOR = exports.MAJOR = exports.QDTEXT = exports.CONNECTION_TOKEN_CHARS = exports.RELAXED_HEADER_CHARS = exports.HEADER_CHARS = exports.HTAB_SP_VCHAR_OBS_TEXT = exports.SP = exports.HTAB = exports.TOKEN = exports.HEX = exports.URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.DIGIT = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.METHODS = exports.METHODS_HTTP = exports.METHODS_HTTP2 = exports.METHODS_HTTP1 = exports.METHODS_RTSP = exports.METHODS_RAOP = exports.METHODS_AIRPLAY = exports.METHODS_ICECAST = exports.METHODS_NON_STANDARD = exports.METHODS_CALDAV = exports.METHODS_UPNP = exports.METHODS_SUBVERSION = exports.METHODS_WEBDAV = exports.METHODS_BASIC_HTTP = exports.METHODS_HTTP1_HEAD = exports.HEADER_STATE = exports.FINISH = exports.STATUSES = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0;
exports.ERROR = {
OK: 0,
INTERNAL: 1,
STRICT: 2,
CR_EXPECTED: 25,
LF_EXPECTED: 3,
UNEXPECTED_CONTENT_LENGTH: 4,
UNEXPECTED_SPACE: 30,
CLOSED_CONNECTION: 5,
INVALID_METHOD: 6,
INVALID_URL: 7,
INVALID_CONSTANT: 8,
INVALID_VERSION: 9,
INVALID_HEADER_TOKEN: 10,
INVALID_CONTENT_LENGTH: 11,
INVALID_CHUNK_SIZE: 12,
INVALID_STATUS: 13,
INVALID_EOF_STATE: 14,
INVALID_TRANSFER_ENCODING: 15,
CB_MESSAGE_BEGIN: 16,
CB_HEADERS_COMPLETE: 17,
CB_MESSAGE_COMPLETE: 18,
CB_CHUNK_HEADER: 19,
CB_CHUNK_COMPLETE: 20,
PAUSED: 21,
PAUSED_UPGRADE: 22,
PAUSED_H2_UPGRADE: 23,
USER: 24,
CB_URL_COMPLETE: 26,
CB_STATUS_COMPLETE: 27,
CB_METHOD_COMPLETE: 32,
CB_VERSION_COMPLETE: 33,
CB_HEADER_FIELD_COMPLETE: 28,
CB_HEADER_VALUE_COMPLETE: 29,
CB_CHUNK_EXTENSION_NAME_COMPLETE: 34,
CB_CHUNK_EXTENSION_VALUE_COMPLETE: 35,
CB_RESET: 31,
CB_PROTOCOL_COMPLETE: 38
};
exports.TYPE = {
BOTH: 0,
REQUEST: 1,
RESPONSE: 2
};
exports.FLAGS = {
CONNECTION_KEEP_ALIVE: 1,
CONNECTION_CLOSE: 2,
CONNECTION_UPGRADE: 4,
CHUNKED: 8,
UPGRADE: 16,
CONTENT_LENGTH: 32,
SKIPBODY: 64,
TRAILING: 128,
TRANSFER_ENCODING: 512
};
exports.LENIENT_FLAGS = {
HEADERS: 1,
CHUNKED_LENGTH: 2,
KEEP_ALIVE: 4,
TRANSFER_ENCODING: 8,
VERSION: 16,
DATA_AFTER_CLOSE: 32,
OPTIONAL_LF_AFTER_CR: 64,
OPTIONAL_CRLF_AFTER_CHUNK: 128,
OPTIONAL_CR_BEFORE_LF: 256,
SPACES_AFTER_CHUNK_SIZE: 512,
HEADER_VALUE_RELAXED: 1024
};
exports.STATUSES = {
CONTINUE: 100,
SWITCHING_PROTOCOLS: 101,
PROCESSING: 102,
EARLY_HINTS: 103,
RESPONSE_IS_STALE: 110,
REVALIDATION_FAILED: 111,
DISCONNECTED_OPERATION: 112,
HEURISTIC_EXPIRATION: 113,
MISCELLANEOUS_WARNING: 199,
OK: 200,
CREATED: 201,
ACCEPTED: 202,
NON_AUTHORITATIVE_INFORMATION: 203,
NO_CONTENT: 204,
RESET_CONTENT: 205,
PARTIAL_CONTENT: 206,
MULTI_STATUS: 207,
ALREADY_REPORTED: 208,
TRANSFORMATION_APPLIED: 214,
IM_USED: 226,
MISCELLANEOUS_PERSISTENT_WARNING: 299,
MULTIPLE_CHOICES: 300,
MOVED_PERMANENTLY: 301,
FOUND: 302,
SEE_OTHER: 303,
NOT_MODIFIED: 304,
USE_PROXY: 305,
SWITCH_PROXY: 306,
TEMPORARY_REDIRECT: 307,
PERMANENT_REDIRECT: 308,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
PAYMENT_REQUIRED: 402,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
NOT_ACCEPTABLE: 406,
PROXY_AUTHENTICATION_REQUIRED: 407,
REQUEST_TIMEOUT: 408,
CONFLICT: 409,
GONE: 410,
LENGTH_REQUIRED: 411,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
URI_TOO_LONG: 414,
UNSUPPORTED_MEDIA_TYPE: 415,
RANGE_NOT_SATISFIABLE: 416,
EXPECTATION_FAILED: 417,
IM_A_TEAPOT: 418,
PAGE_EXPIRED: 419,
ENHANCE_YOUR_CALM: 420,
MISDIRECTED_REQUEST: 421,
UNPROCESSABLE_ENTITY: 422,
LOCKED: 423,
FAILED_DEPENDENCY: 424,
TOO_EARLY: 425,
UPGRADE_REQUIRED: 426,
PRECONDITION_REQUIRED: 428,
TOO_MANY_REQUESTS: 429,
REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL: 430,
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
LOGIN_TIMEOUT: 440,
NO_RESPONSE: 444,
RETRY_WITH: 449,
BLOCKED_BY_PARENTAL_CONTROL: 450,
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
CLIENT_CLOSED_LOAD_BALANCED_REQUEST: 460,
INVALID_X_FORWARDED_FOR: 463,
REQUEST_HEADER_TOO_LARGE: 494,
SSL_CERTIFICATE_ERROR: 495,
SSL_CERTIFICATE_REQUIRED: 496,
HTTP_REQUEST_SENT_TO_HTTPS_PORT: 497,
INVALID_TOKEN: 498,
CLIENT_CLOSED_REQUEST: 499,
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
HTTP_VERSION_NOT_SUPPORTED: 505,
VARIANT_ALSO_NEGOTIATES: 506,
INSUFFICIENT_STORAGE: 507,
LOOP_DETECTED: 508,
BANDWIDTH_LIMIT_EXCEEDED: 509,
NOT_EXTENDED: 510,
NETWORK_AUTHENTICATION_REQUIRED: 511,
WEB_SERVER_UNKNOWN_ERROR: 520,
WEB_SERVER_IS_DOWN: 521,
CONNECTION_TIMEOUT: 522,
ORIGIN_IS_UNREACHABLE: 523,
TIMEOUT_OCCURED: 524,
SSL_HANDSHAKE_FAILED: 525,
INVALID_SSL_CERTIFICATE: 526,
RAILGUN_ERROR: 527,
SITE_IS_OVERLOADED: 529,
SITE_IS_FROZEN: 530,
IDENTITY_PROVIDER_AUTHENTICATION_ERROR: 561,
NETWORK_READ_TIMEOUT: 598,
NETWORK_CONNECT_TIMEOUT: 599
};
exports.FINISH = {
SAFE: 0,
SAFE_WITH_CB: 1,
UNSAFE: 2
};
exports.HEADER_STATE = {
GENERAL: 0,
CONNECTION: 1,
CONTENT_LENGTH: 2,
TRANSFER_ENCODING: 3,
UPGRADE: 4,
CONNECTION_KEEP_ALIVE: 5,
CONNECTION_CLOSE: 6,
CONNECTION_UPGRADE: 7,
TRANSFER_ENCODING_CHUNKED: 8
};
exports.METHODS_HTTP1_HEAD = { HEAD: 2 };
/**
* HTTP methods as defined by RFC-9110 and other specifications.
* @see https://httpwg.org/specs/rfc9110.html#method.definitions
*/
exports.METHODS_BASIC_HTTP = {
DELETE: 0,
GET: 1,
...exports.METHODS_HTTP1_HEAD,
POST: 3,
PUT: 4,
CONNECT: 5,
OPTIONS: 6,
TRACE: 7,
/**
* @see https://www.rfc-editor.org/rfc/rfc5789.html
*/
PATCH: 28,
LINK: 31,
UNLINK: 32
};
exports.METHODS_WEBDAV = {
COPY: 8,
LOCK: 9,
MKCOL: 10,
MOVE: 11,
PROPFIND: 12,
PROPPATCH: 13,
SEARCH: 14,
UNLOCK: 15,
BIND: 16,
REBIND: 17,
UNBIND: 18,
ACL: 19
};
exports.METHODS_SUBVERSION = {
REPORT: 20,
MKACTIVITY: 21,
CHECKOUT: 22,
MERGE: 23
};
exports.METHODS_UPNP = {
"M-SEARCH": 24,
NOTIFY: 25,
SUBSCRIBE: 26,
UNSUBSCRIBE: 27
};
exports.METHODS_CALDAV = { MKCALENDAR: 30 };
exports.METHODS_NON_STANDARD = {
/**
* Not defined in any RFC but commonly used
*/
PURGE: 29,
QUERY: 46
};
exports.METHODS_ICECAST = { SOURCE: 33 };
exports.METHODS_AIRPLAY = {
GET: 1,
POST: 3
};
exports.METHODS_RAOP = { FLUSH: 45 };
exports.METHODS_RTSP = {
OPTIONS: exports.METHODS_BASIC_HTTP.OPTIONS,
DESCRIBE: 35,
ANNOUNCE: 36,
SETUP: 37,
PLAY: 38,
PAUSE: 39,
TEARDOWN: 40,
GET_PARAMETER: 41,
SET_PARAMETER: 42,
REDIRECT: 43,
RECORD: 44,
...exports.METHODS_AIRPLAY,
...exports.METHODS_RAOP
};
exports.METHODS_HTTP1 = {
...exports.METHODS_BASIC_HTTP,
...exports.METHODS_WEBDAV,
...exports.METHODS_SUBVERSION,
...exports.METHODS_UPNP,
...exports.METHODS_CALDAV,
...exports.METHODS_NON_STANDARD,
...exports.METHODS_ICECAST
};
exports.METHODS_HTTP2 = {
/**
* RFC-9113, section 11.6
* @see https://www.rfc-editor.org/rfc/rfc9113.html#preface
*/
PRI: 34 };
exports.METHODS_HTTP = {
...exports.METHODS_HTTP1,
...exports.METHODS_HTTP2
};
exports.METHODS = {
...exports.METHODS_HTTP1,
...exports.METHODS_HTTP2,
...exports.METHODS_RTSP
};
exports.ALPHA = [
"A",
"a",
"B",
"b",
"C",
"c",
"D",
"d",
"E",
"e",
"F",
"f",
"G",
"g",
"H",
"h",
"I",
"i",
"J",
"j",
"K",
"k",
"L",
"l",
"M",
"m",
"N",
"n",
"O",
"o",
"P",
"p",
"Q",
"q",
"R",
"r",
"S",
"s",
"T",
"t",
"U",
"u",
"V",
"v",
"W",
"w",
"X",
"x",
"Y",
"y",
"Z",
"z"
];
exports.NUM_MAP = {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9
};
exports.HEX_MAP = {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
A: 10,
B: 11,
C: 12,
D: 13,
E: 14,
F: 15,
a: 10,
b: 11,
c: 12,
d: 13,
e: 14,
f: 15
};
exports.DIGIT = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9"
];
exports.ALPHANUM = [...exports.ALPHA, ...exports.DIGIT];
exports.MARK = [
"-",
"_",
".",
"!",
"~",
"*",
"'",
"(",
")"
];
exports.USERINFO_CHARS = [
...exports.ALPHANUM,
...exports.MARK,
"%",
";",
":",
"&",
"=",
"+",
"$",
","
];
exports.URL_CHAR = [
"!",
"\"",
"$",
"%",
"&",
"'",
"(",
")",
"*",
"+",
",",
"-",
".",
"/",
":",
";",
"<",
"=",
">",
"@",
"[",
"\\",
"]",
"^",
"_",
"`",
"{",
"|",
"}",
"~",
...exports.ALPHANUM
];
exports.HEX = [
...exports.DIGIT,
"a",
"b",
"c",
"d",
"e",
"f",
"A",
"B",
"C",
"D",
"E",
"F"
];
exports.TOKEN = [
"!",
"#",
"$",
"%",
"&",
"'",
"*",
"+",
"-",
".",
"^",
"_",
"`",
"|",
"~",
...exports.ALPHANUM
];
exports.HTAB = [" "];
exports.SP = [" "];
const VCHAR = [
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126
];
const OBS_TEXT = [
128,
129,
130,
131,
132,
133,
134,
135,
136,
137,
138,
139,
140,
141,
142,
143,
144,
145,
146,
147,
148,
149,
150,
151,
152,
153,
154,
155,
156,
157,
158,
159,
160,
161,
162,
163,
164,
165,
166,
167,
168,
169,
170,
171,
172,
173,
174,
175,
176,
177,
178,
179,
180,
181,
182,
183,
184,
185,
186,
187,
188,
189,
190,
191,
192,
193,
194,
195,
196,
197,
198,
199,
200,
201,
202,
203,
204,
205,
206,
207,
208,
209,
210,
211,
212,
213,
214,
215,
216,
217,
218,
219,
220,
221,
222,
223,
224,
225,
226,
227,
228,
229,
230,
231,
232,
233,
234,
235,
236,
237,
238,
239,
240,
241,
242,
243,
244,
245,
246,
247,
248,
249,
250,
251,
252,
253,
254,
255
];
exports.HTAB_SP_VCHAR_OBS_TEXT = [
...exports.HTAB,
...exports.SP,
...VCHAR,
...OBS_TEXT
];
exports.HEADER_CHARS = exports.HTAB_SP_VCHAR_OBS_TEXT;
exports.RELAXED_HEADER_CHARS = [...[
1,
2,
3,
4,
5,
6,
7,
8,
11,
12,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
127
], ...exports.HEADER_CHARS];
exports.CONNECTION_TOKEN_CHARS = [
...exports.HTAB,
...exports.SP,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126,
...OBS_TEXT
];
exports.QDTEXT = [
...exports.HTAB,
...exports.SP,
33,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126,
...OBS_TEXT
];
exports.MAJOR = exports.NUM_MAP;
exports.MINOR = exports.MAJOR;
exports.SPECIAL_HEADERS = {
"connection": exports.HEADER_STATE.CONNECTION,
"content-length": exports.HEADER_STATE.CONTENT_LENGTH,
"proxy-connection": exports.HEADER_STATE.CONNECTION,
"transfer-encoding": exports.HEADER_STATE.TRANSFER_ENCODING,
"upgrade": exports.HEADER_STATE.UPGRADE
};
exports.default = {
ERROR: exports.ERROR,
TYPE: exports.TYPE,
FLAGS: exports.FLAGS,
LENIENT_FLAGS: exports.LENIENT_FLAGS,
STATUSES: exports.STATUSES,
FINISH: exports.FINISH,
HEADER_STATE: exports.HEADER_STATE,
ALPHA: exports.ALPHA,
NUM_MAP: exports.NUM_MAP,
HEX_MAP: exports.HEX_MAP,
DIGIT: exports.DIGIT,
ALPHANUM: exports.ALPHANUM,
MARK: exports.MARK,
USERINFO_CHARS: exports.USERINFO_CHARS,
URL_CHAR: exports.URL_CHAR,
HEX: exports.HEX,
TOKEN: exports.TOKEN,
HEADER_CHARS: exports.HEADER_CHARS,
RELAXED_HEADER_CHARS: exports.RELAXED_HEADER_CHARS,
CONNECTION_TOKEN_CHARS: exports.CONNECTION_TOKEN_CHARS,
QDTEXT: exports.QDTEXT,
HTAB_SP_VCHAR_OBS_TEXT: exports.HTAB_SP_VCHAR_OBS_TEXT,
MAJOR: exports.MAJOR,
MINOR: exports.MINOR,
SPECIAL_HEADERS: exports.SPECIAL_HEADERS,
METHODS: exports.METHODS,
METHODS_HTTP: exports.METHODS_HTTP,
METHODS_HTTP1_HEAD: exports.METHODS_HTTP1_HEAD,
METHODS_HTTP1: exports.METHODS_HTTP1,
METHODS_HTTP2: exports.METHODS_HTTP2,
METHODS_ICECAST: exports.METHODS_ICECAST,
METHODS_RTSP: exports.METHODS_RTSP
};
})))(), 1);
const KIND_REQUEST = import_constants.TYPE.REQUEST;
import_constants.TYPE.RESPONSE;
const kPointer = Symbol("kPtr");
const kUrl = Symbol("kUrl");
const kStatusMessage = Symbol("kStatusMessage");
const kHeadersFields = Symbol("kHeadersFields");
const kHeadersValues = Symbol("kHeadersValues");
const kLastHeaderCallback = Symbol("kLastHeaderCallback");
const kCallbacks = Symbol("kCallbacks");
const kType = Symbol("kType");
const HEADER_CB_NONE = 0;
const HEADER_CB_FIELD = 1;
const HEADER_CB_VALUE = 2;
const parsersMap = /* @__PURE__ */ new Map();
const methodNames = Object.fromEntries(Object.entries(import_constants.METHODS).map(([name, num]) => [num, name]));
function readStringFrom(pointer, length) {
return Buffer.from(llhttp_memory.buffer, pointer, length).toString("latin1");
}
/**
* @note Reference the base URL through a variable. Bundlers (e.g. Vite)
* statically rewrite the `new URL('...', import.meta.url)` pattern into
* an asset URL resolved against the served origin, which breaks reading
* the WASM binary from the file system in DOM-like test environments.
*/
const wasmBaseUrl = import.meta.url;
const llhttpModule = new WebAssembly.Module(fs.readFileSync(new URL("./llhttp/llhttp.wasm", wasmBaseUrl)));
const llhttpInstance = new WebAssembly.Instance(llhttpModule, { env: {
wasm_on_message_begin(parserPointer) {
const parser = parsersMap.get(parserPointer);
parser[kUrl] = "";
parser[kStatusMessage] = "";
parser[kHeadersFields] = [];
parser[kHeadersValues] = [];
parser[kLastHeaderCallback] = HEADER_CB_NONE;
return parser[kCallbacks].onMessageBegin?.() ?? 0;
},
wasm_on_url(parserPointer, at, length) {
parsersMap.get(parserPointer)[kUrl] = readStringFrom(at, length);
return 0;
},
wasm_on_status(parserPointer, at, length) {
parsersMap.get(parserPointer)[kStatusMessage] = readStringFrom(at, length);
return 0;
},
wasm_on_header_field(parserPointer, at, length) {
const parser = parsersMap.get(parserPointer);
const chunk = readStringFrom(at, length);
const fields = parser[kHeadersFields];
if (parser[kLastHeaderCallback] === HEADER_CB_FIELD) fields[fields.length - 1] += chunk;
else {
fields.push(chunk);
parser[kLastHeaderCallback] = HEADER_CB_FIELD;
}
return 0;
},
wasm_on_header_value(parserPointer, at, length) {
const parser = parsersMap.get(parserPointer);
const chunk = readStringFrom(at, length);
const values = parser[kHeadersValues];
if (parser[kLastHeaderCallback] === HEADER_CB_VALUE) values[values.length - 1] += chunk;
else {
values.push(chunk);
parser[kLastHeaderCallback] = HEADER_CB_VALUE;
}
return 0;
},
wasm_on_headers_complete(parserPointer, statusCode, rawUpgrade, rawShouldKeepAlive) {
const parser = parsersMap.get(parserPointer);
const versionMajor = llhttp_get_version_major(parserPointer);
const versionMinor = llhttp_get_version_minor(parserPointer);
const rawHeaders = [];
const upgrade = rawUpgrade === 1;
const shouldKeepAlive = rawShouldKeepAlive === 1;
for (let c = 0; c < parser[kHeadersFields].length; c++) rawHeaders.push(parser[kHeadersFields][c], parser[kHeadersValues][c]);
if (parser[kType] === KIND_REQUEST) {
const method = methodNames[llhttp_get_method(parserPointer)];
const url = parser[kUrl];
return parser[kCallbacks].onHeadersComplete?.({
versionMajor,
versionMinor,
rawHeaders,
method,
url,
upgrade,
shouldKeepAlive
}) ?? 0;
} else {
const statusCode = llhttp_get_status_code(parserPointer);
const statusMessage = parser[kStatusMessage];
return parser[kCallbacks].onHeadersComplete?.({
versionMajor,
versionMinor,
rawHeaders,
statusCode,
statusMessage,
upgrade,
shouldKeepAlive
}) ?? 0;
}
},
wasm_on_body(parserPointer, at, length) {
const parser = parsersMap.get(parserPointer);
const body = Buffer.from(new Uint8Array(llhttp_memory.buffer, at, length));
return parser[kCallbacks].onBody?.(body) ?? 0;
},
wasm_on_message_complete(parserPointer) {
return parsersMap.get(parserPointer)[kCallbacks].onMessageComplete?.() ?? 0;
}
} });
const llhttp_memory = llhttpInstance.exports.memory;
const llhttp_alloc = llhttpInstance.exports.llhttp_alloc;
const llhttp_malloc = llhttpInstance.exports.malloc;
const llhttp_execute = llhttpInstance.exports.llhttp_execute;
llhttpInstance.exports.llhttp_get_type;
llhttpInstance.exports.llhttp_get_upgrade;
llhttpInstance.exports.llhttp_should_keep_alive;
const llhttp_get_method = llhttpInstance.exports.llhttp_get_method;
const llhttp_get_status_code = llhttpInstance.exports.llhttp_get_status_code;
const llhttp_get_version_minor = llhttpInstance.exports.llhttp_get_http_minor;
const llhttp_get_version_major = llhttpInstance.exports.llhttp_get_http_major;
const llhttp_get_error_reason = llhttpInstance.exports.llhttp_get_error_reason;
const llhttp_get_error_pos = llhttpInstance.exports.llhttp_get_error_pos;
const llhttp_free = llhttpInstance.exports.free;
const initialize = llhttpInstance.exports._initialize;
initialize();
var HttpParser = class {
constructor(type, callbacks) {
this[kUrl] = "";
this[kStatusMessage] = "";
this[kHeadersFields] = [];
this[kHeadersValues] = [];
this[kLastHeaderCallback] = HEADER_CB_NONE;
this[kType] = type;
this[kCallbacks] = callbacks;
const parserPointer = llhttp_alloc(type);
if (parserPointer === 0) throw new Error("Failed to allocate llhttp parser");
this[kPointer] = parserPointer;
parsersMap.set(this[kPointer], this);
}
destroy() {
if (this[kPointer] === 0) return;
parsersMap.delete(this[kPointer]);
llhttp_free(this[kPointer]);
this[kPointer] = 0;
}
execute(data) {
const pointer = llhttp_malloc(data.byteLength);
if (pointer === 0) throw new Error("Failed to allocate llhttp input buffer");
let ret;
try {
new Uint8Array(llhttp_memory.buffer).set(data, pointer);
ret = llhttp_execute(this[kPointer], pointer, data.byteLength);
} catch (error) {
llhttp_free(pointer);
throw error;
}
if (ret === import_constants.ERROR.PAUSED_UPGRADE) {
const consumed = llhttp_get_error_pos(this[kPointer]) - pointer;
llhttp_free(pointer);
return data.subarray(consumed);
}
llhttp_free(pointer);
this.#checkError(ret);
return null;
}
#checkError(errorCode) {
if (errorCode === import_constants.ERROR.OK) return;
const errorPointer = llhttp_get_error_reason(this[kPointer]);
const length = new Uint8Array(llhttp_memory.buffer).indexOf(0, errorPointer) - errorPointer;
throw new Error(readStringFrom(errorPointer, length));
}
};
//#endregion
//#region src/interceptors/http/http-parser.ts
var HttpRequestParser = class extends HttpParser {
#requestBodyStream;
#upgrade = false;
constructor(options) {
super(1, {
onHeadersComplete: ({ rawHeaders, method, url: path, upgrade }) => {
this.#upgrade = upgrade;
/**
* @note When the socket is reused, "connectionOptions" will point
* to the "net.connect()" call options that established the connection,
* which may differ from the description of the current request (e.g. method).
* Rely on the HTTPParser supplying us with the correct "rawMethod" number.
*/
const finalMethod = (method || options.connectionOptions.method || "GET").toUpperCase();
const url = new URL(path || "", options.connectionOptions.url);
const headers = FetchResponse.parseRawHeaders([...rawHeaders]);
if (url.username || url.password) {
if (!headers.has("authorization")) {
const credentials = Buffer.from(`${url.username}:${url.password}`).toString("base64");
headers.set("authorization", `Basic ${credentials}`);
}
url.username = "";
url.password = "";
}
this.#requestBodyStream = new Readable({
/**
* @note Provide the `read()` method so a `Readable` could be
* used as the actual request body (the stream calls "read()").
*/
read: () => {} });
/**
* @note Expose an abort controller for the parsed request so the
* consumer can abort it (e.g. when the client destroys the
* connection before the request is handled).
*/
const abortController = new AbortController();
const request = new FetchRequest(url, {
method: finalMethod,
headers,
credentials: "same-origin",
body: Readable.toWeb(this.#requestBodyStream),
signal: abortController.signal
});
options.onRequest(request, abortController);
},
onBody: (chunk) => {
invariant(this.#requestBodyStream, "Failed to write to a request stream: stream does not exist. This is likely an issue with the library. Please report it on GitHub.");
this.#requestBodyStream.push(chunk);
},
onMessageComplete: () => {
this.#requestBodyStream?.push(null);
/**
* @note An upgraded exchange (e.g. "CONNECT", WebSocket) has
* no message boundary: it takes the connection over, and the
* bytes that follow belong to it, not to a next exchange.
*/
if (!this.#upgrade) options.onMessageComplete?.();
}
});
}
free() {
this.destroy();
this.#requestBodyStream?.destroy();
this.#requestBodyStream = void 0;
}
};
var HttpResponseParser = class extends HttpParser {
#responseBodyStream;
constructor(options) {
super(2, {
onHeadersComplete: ({ rawHeaders, statusCode: status, statusMessage: statusText }) => {
const headers = FetchResponse.parseRawHeaders([...rawHeaders]);
const response = new FetchResponse(FetchResponse.isResponseWithBody(status) ? Readable.toWeb(this.#responseBodyStream = new Readable({ read() {} })) : null, {
status,
statusText,
headers
});
options.onResponse(response);
},
onBody: (chunk) => {
invariant(this.#responseBodyStream, "Failed to read from a response stream: stream does not exist. This is likely an issue with the library. Please report it on GitHub.");
this.#responseBodyStream.push(chunk);
},
onMessageComplete: () => {
this.#responseBodyStream?.push(null);
}
});
}
free() {
this.destroy();
this.#responseBodyStream = null;
}
};
//#endregion
//#region src/utils/is-node-like-error.ts
function isNodeLikeError(error) {
if (error == null) return false;
if (!(error instanceof Error)) return false;
return "code" in error && "errno" in error;
}
//#endregion
//#region src/utils/handle-request.ts
async function handleRequest(options) {
if (options.logger?.isEnabled("default")) formatRequest(options.request).then((message) => {
options.logger?.info("[%s] %s", options.requestId, message);
});
const handleResponse = async (response) => {
if (response instanceof Error) {
await options.controller.errorWith(response);
return true;
}
if (isResponseError(response)) {
await options.controller.respondWith(response);
return true;
}
/**
* Handle normal responses or response-like objects.
* @note This must come before the arbitrary object check
* since Response instances are, in fact, objects.
*/
if (isResponseLike(response)) {
await options.controller.respondWith(response);
return true;
}
if (isObject(response)) {
await options.controller.errorWith(response);
return true;
}
return false;
};
const handleResponseError = async (error) => {
if (error instanceof InterceptorError) throw resultError;
if (isNodeLikeError(error)) {
await options.controller.errorWith(error);
return true;
}
if (error instanceof Response) return await handleResponse(error);
return false;
};
const requestAbortPromise = Promise.withResolvers();
let requestAbortReason;
let isRequestAborted = false;
const onAbort = () => {
isRequestAborted = true;
requestAbortReason = options.request.signal?.reason;
requestAbortPromise.reject(requestAbortReason);
};
/**
* @note `signal` is not always defined in React Native.
*/
if (options.request.signal) {
if (options.request.signal.aborted) {
await options.controller.errorWith(options.request.signal.reason);
return;
}
options.request.signal.addEventListener("abort", onAbort, { once: true });
}
const [resultError] = await until(async () => {
const requestEvent = new HttpRequestEvent({
initiator: options.initiator,
requestId: options.requestId,
request: options.request,
controller: options.controller
});
const requestListenersPromise = options.emitter.emitAsPromise(requestEvent);
await Promise.race([
requestAbortPromise.promise,
requestListenersPromise,
options.controller.handled
]);
/**
* @note If the "request" listener has replaced the request instance,
* propagate that mutation back to the underlying insterceptor.
* This happens with XMLHttpRequest that replaces request instances
* to correctly reflect the "withCredentials" option on the Fetch API request.
*/
if (requestEvent.request !== options.request) options.request = requestEvent.request;
});
options.request.signal?.removeEventListener("abort", onAbort);
if (isRequestAborted) {
await options.controller.errorWith(requestAbortReason);
return;
}
if (resultError) {
if (await handleResponseError(resultError)) return;
if (options.emitter.listenerCount("unhandledException") > 0) {
const unhandledExceptionController = new RequestController(options.request, {
/**
* @note Intentionally empty passthrough handle.
* This controller is created within another controller and we only need
* to know if `unhandledException` listeners handled the request.
*/
passthrough() {},
async respondWith(response) {
await handleResponse(response);
},
async errorWith(reason) {
/**
* @note Handle the result of the unhandled controller
* in the same way as the original request controller.
* The exception here is that thrown errors within the
* "unhandledException" event do NOT result in another
* emit of the same event. They are forwarded as-is.
*/
await options.controller.errorWith(reason);
}
});
await options.emitter.emitAsPromise(new UnhandledHttpException({
initiator: options.initiator,
error: resultError,
request: options.request,
requestId: options.requestId,
controller: unhandledExceptionController
}));
if (unhandledExceptionController.readyState !== RequestController.PENDING) return;
}
await options.controller.respondWith(createServerErrorResponse(resultError));
return;
}
if (options.controller.readyState === RequestController.PENDING) return await options.controller.passthrough();
return options.controller.handled;
}
//#endregion
//#region src/interceptors/http/source.ts
const httpLogger = createLogger("http-request");
/**
* Interceptor for HTTP requests in Node.js.
* Routes socket connections through an HTTP parser.
*/
var NodeHttpRequestSource = class extends Interceptor {
static {
this.symbol = Symbol.for("node-http-request-source");
}
predicate() {
return true;
}
setup() {
const socketInterceptor = Interceptor.singleton(SocketInterceptor);
socketInterceptor.apply(this);
this.subscriptions.push(() => {
socketInterceptor.dispose(this);
});
/**
* @note Record the raw values provided to Headers set/append
* in order to support "IncomingMessage.prototype.rawHeaders".
* This is meant for the headers in mocked responses.
*/
this.subscriptions.push(recordRawFetchHeaders());
const controller = new AbortController();
this.subscriptions.push(() => controller.abort());
socketInterceptor.on("connection", ({ connectionOptions, socket, controller: socketController }) => {
let isHttpConnection;
let requestParser;
let tunnelUrl;
let abortPendingRequest;
/**
* @note Capture the request context of the connection itself.
* The socket is created synchronously within the request async
* context (e.g. inside the patched `http.request()`), but the
* first data may reach the socket from a foreign context (e.g.
* a form-data stream piped into the request), where sampling
* the request context yields nothing.
*/
const connectionRequestContext = requestContext.getStore();
/**
* @note The client destroys the socket synchronously (e.g. Undici
* on request abort) but the socket teardown events ("error",
* "close") are emitted asynchronously, after the consumer has
* already observed the rejected request promise. Hook into the
* destroy itself so the pending request is aborted before any
* of its listeners can resume.
*/
const rawSocket = socketController[kRawSocket];
const realSocketDestroy = rawSocket._destroy.bind(rawSocket);
rawSocket._destroy = (error, callback) => {
abortPendingRequest?.();
realSocketDestroy(error, callback);
};
/**
* @note Only inspect the first sent packet to determine the protocol.
* A single socket cannot be used for different protocols.
*/
socket.on("data", (chunk) => {
if (isHttpConnection === false) return;
/**
* @note A mocked "CONNECT" request has established a tunnel.
* The data that follows belongs to a new exchange addressed to
* the tunnel target. The parser stopped at the tunnel boundary
* (HTTP upgrade semantics), so tear it down and detect the
* tunneled protocol anew, like on a fresh connection.
*/
if (tunnelUrl && requestParser) {
requestParser.free();
requestParser = void 0;
isHttpConnection = void 0;
/**
* @note Retarget the connection to the tunnel authority.
* The exchanges that follow belong to the tunnel target,
* so an unclaimed exchange (HTTP or not) must pass through
* to that target — not to the proxy, which never actually
* established this tunnel — like a real established tunnel
* relays its traffic.
*/
socketController.reset({
host: tunnelUrl.hostname,
port: Number(tunnelUrl.port) || 80,
path: null
});
}
if (requestParser) {
requestParser.execute(toBuffer(chunk));
return;
}
const httpMessage = chunk.toString();
const httpMethod = httpMessage.split(" ")[0] || "";
if (!METHODS.includes(httpMethod.toUpperCase())) {
isHttpConnection = false;
socketController.decline();
return;
}
isHttpConnection = true;
const baseUrl = tunnelUrl ?? connectionOptionsToUrl(connectionOptions, socket);
httpLogger.verbose("handling http message %o", {
httpMessage,
httpMethod,
baseUrl
});
const requestContextValue = requestContext.getStore() ?? connectionRequestContext;
const initiator = requestContextValue?.initiator || socket;
requestParser = new HttpRequestParser({
connectionOptions: {
method: httpMethod,
url: baseUrl
},
/**
* @note The message boundary ends the current exchange.
* Schedule the controller reset so the next write on this
* (kept-alive) socket opens a new exchange and buffers for
* its own verdict instead of following the settled one
* (e.g. leaking a mocked request to the server of a
* previously passed-through exchange).
*/
onMessageComplete: () => {
socketController.scheduleReset();
},
onRequest: async (parsedRequest, requestAbortController) => {
const request = requestContextValue?.transformRequest?.(parsedRequest) ?? parsedRequest;
/**
* @note A subsequent request arriving on a kept-alive socket
* that has already been handled (passed through or mocked).
* Clients like Undici reuse sockets without emitting the
* "free" event, so reset the controller here, at the HTTP
* message boundary, to handle the new request from the
* pending state again.
*/
if (socketController["readyState"] !== SocketController.PENDING) socketController.reset();
const requestId = createRequestId();
const requestLogger = requestContextValue?.logger ?? httpLogger;
httpLogger.verbose("received a parsed HTTP request %o", {
method: request.method,
url: request.url
});
const requestController = new RequestController(request, {
respondWith: async (rawResponse) => {
httpLogger.verbose("respondWith() %o", {
status: rawResponse.status,
statusText: rawResponse.statusText,
hasBody: rawResponse.body != null
});
/**
* @note The client may destroy the socket (e.g. on request
* abort) moments before a response arrives. A destroyed
* socket cannot be claimed and has no one reading it.
*/
if (socket.destroyed) return;
socketController.claim();
const response = FetchResponse.from(rawResponse, { url: request.url });
/**
* @note A successful mocked response to a "CONNECT"
* request establishes a tunnel to the requested authority
* (e.g. "127.0.0.1:80"). The exchange that follows on this
* socket is addressed to that authority, not to the proxy.
*/
if (request.method === "CONNECT" && response.ok) tunnelUrl = new URL(`http://${request.url}`);
/**
* @note Clone the response before "respondWith" because it will
* consume its body. This way, we can have a readable response copy
* for the "response" event below.
*/
const responseClone = isResponseError(response) ? null : response.clone();
const respond = () => {
return this.respondWith({
socket: socketController[kRawSocket],
request: context.request,
response
});
};
if (responseClone) await this.emitter.emitAsPromise(new HttpResponseEvent({
initiator,
requestId,
request: context.request,
response: responseClone,
responseType: "mock"
}));
if (socket.connecting) socket.once("connect", respond);
else
/**
* @note Reused sockets stay connected between requests and will not
* emit "connect" anymore. If that's the case, respond immediately.
*/
await respond();
},
errorWith: (reason) => {
if (reason instanceof Error) socket.destroy(reason);
},
passthrough: () => {
const realSocket = socketController.passthrough(this.#modifyHttpHeaders(context.request));
if (this.emitter.listenerCount("response") > 0) {
httpLogger.verbose("found \"response\" listener, corking socket reads");
/**
* Suspend the delivery of the original response to the client
* until the "response" event listeners settle. This guarantees
* that the request promise (e.g. `await fetch()`) does not
* resolve before the listeners are done. The real socket keeps
* emitting data for the response parser meanwhile.
*/
socketController.corkReads();
const responseParser = new HttpResponseParser({ onResponse: async (response) => {
httpLogger.verbose("HTTP response parser parsed: %d %s", response.status, response.statusText);
if (isResponseError(response)) {
httpLogger.verbose("response is an error response, uncorking socket reads...");
socketController.uncorkReads();
return;
}
FetchResponse.setUrl(request.url, response);
try {
httpLogger.verbose("emitting \"response\" event");
await this.emitter.emitAsPromise(new HttpResponseEvent({
initiator,
requestId,
request: context.request,
response,
responseType: "original"
}));
} finally {
httpLogger.verbose("uncorking socket reads");
socketController.uncorkReads();
/**
* @note Informational responses other than
* "101 Switching Protocols" are followed by a final
* response on the same connection. Keep gating that
* final response on the "response" event listeners.
*/
if (response.status < 200 && response.status !== 101) socketController.corkReads();
}
} });
realSocket.on("data", (chunk) => responseParser.execute(chunk)).on("close", () => responseParser.free());
}
}
}, {
logger: requestLogger,
requestId
});
invariant(socketController["readyState"] === SocketController.PENDING, "CANNOT HANDLE ALREADY HANDLED REQUEST", request.method, request.url, socketController["readyState"]);
/**
* @note Create a request resolution context.
* This is so modifications to the "request" in upstream interceptors
* are correctly picked up by the underlying HTTP interceptor.
*/
const context = {
initiator,
requestId,
request,
controller: requestController,
emitter: this.emitter,
logger: requestLogger
};
/**
* @note The client destroying the socket while the request
* is still pending means the request was aborted (e.g. via
* `AbortController`). Abort the parsed request so its
* handling settles and late interactions with the request
* controller become controlled errors.
*/
abortPendingRequest = () => {
if (requestController.readyState === RequestController.PENDING) requestAbortController.abort();
};
try {
await handleRequest(context);
} finally {
abortPendingRequest = void 0;
}
}
});
requestParser.execute(toBuffer(chunk));
});
socket.on("close", () => requestParser?.free());
}, { signal: controller.signal });
}
async respondWith(args) {
const { socket, request, response } = args;
if (socket.destroyed) return;
if (isResponseError(response)) {
/**
* @note Reference the error response on the socket error so the
* client-side interceptors (e.g. fetch) can surface it to the
* consumer as the reason behind the failed request. Keep the
* reference non-enumerable so the error remains observably
* identical for the clients that expose it as-is.
*/
socket.destroy(Object.defineProperty(/* @__PURE__ */ new TypeError("Network error"), kErrorResponse, {
value: response,
enumerable: false
}));
return;
}
invariant(!socket.connecting, "Failed to mock a response for \"%s %s\": socket has not connected", request.method, request.url);
/**
* Use native server response handling in Node.js.
* @see https://github.com/nodejs/node/blob/13eb80f3b718452213e0fc449702aefbbfe4110f/lib/_http_server.js#L202
*/
const incomingMessage = new IncomingMessage(socket);
/**
* @note Describe the request method so the response body is
* handled appropriately (e.g. "HEAD" responses must not write
* a body). The HTTP version is deliberately left unset: with it,
* `ServerResponse` frames bodies of unknown length as chunked,
* polluting the mocked response headers with "Transfer-Encoding"
* the mock never specified.
*/
incomingMessage.method = request.method;
const serverResponse = new ServerResponse(incomingMessage);
const responseSocket = new net.Socket();
responseSocket._writeGeneric = (writev, data, encoding, callback) => {
unwrapPendingData(data, (chunk, encoding) => {
socket.push(toBuffer(chunk), encoding);
});
callback?.();
};
responseSocket._destroy = (error, callback) => {
/**
* Only destroy the socket on stream errors.
* On a clean end, the socket is already signaled via `socket.push(null)`
* in the main response flow. Destroying it here prematurely would prevent
* the client from processing the response (e.g. calling `response.destroy()`).
* @see https://github.com/mswjs/interceptors/issues/738
*/
if (error) socket.destroy();
callback(null);
};
responseSocket.on("drain", () => serverResponse.emit("drain"));
serverResponse.assignSocket(responseSocket);
serverResponse.removeHeader("connection");
serverResponse.removeHeader("date");
const rawResponseHeaders = getRawFetchHeaders(response.headers);
serverResponse.writeHead(response.status, response.statusText || STATUS_CODES[response.status], rawResponseHeaders);
/**
* @note Override the socket's `_destroy` before writing the response body.
* The underlying TCP handle (from `socket.connect()`) makes `_destroy` async
* (`_handle.close()` callback), which delays the 'error' event. Since the real
* TCP connection is irrelevant for mocked responses, take the synchronous path
* so that user-initiated `response.destroy(error)` emits the error promptly.
* This must happen before `serverResponse.end()` because the HTTP parser may
* fire the 'response' event synchronously during `socket.push()`.
*/
socket._destroy = function(error, callback) {
if (error)
/**
* Emit the error event as a microtask instead of relying on the default
* `process.nextTick(emitErrorNT)` from `callback(error)`. This is necessary
* because `respondWith` runs inside a microtask (from `await reader.read()`).
* A resolved promise continuation (from toWebResponse) is queued as
* another microtask during the same phase. Since microtasks are drained before
* nextTick, the test's `await` would resolve before the error event fires.
* Using `queueMicrotask` ensures the error event is emitted within the current
* microtask phase, before other queued microtasks.
*/
queueMicrotask(() => this.emit("error", error));
callback(null);
/**
* @note `net.Socket` is constructed with `emitClose: false`, so Node's
* stream destroy machinery does not emit `'close'` automatically; the
* stock `net.Socket._destroy` only emits it via `_handle.close()`.
* Since this override replaces `_destroy`, emit `'close'` here so the
* mocked socket completes its lifecycle (otherwise consumers waiting
* on `'close'`, like `http.ClientRequest`, hang).
*/
process.nextTick(() => this.emit("close", error != null));
};
if (response.body) {
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
serverResponse.end();
break;
}
if (!serverResponse.write(value)) await new Promise((resolve) => {
serverResponse.once("drain", resolve);
});
}
} catch {
/**
* @note Delay the socket destruction to allow the event loop
* to flush already-pushed response data (headers + body chunks)
* through the HTTP parser. Without this, the socket is destroyed
* on the same tick as `socket.push(data)` and the client never
* reads the response.
*/
await new Promise((resolve) => process.nextTick(resolve));
socket.destroy();
return;
}
} else serverResponse.end();
/**
* @note Self-delimiting responses (chunked, explicit "Content-Length",
* or bodiless by definition) must NOT signal the end-of-stream.
* The client parser completes them from their framing alone, and
* ending the socket would kill the kept-alive connection that
* agents pool and reuse for subsequent requests.
*/
const isSelfDelimitingResponse = request.method === "HEAD" || response.headers.has("content-length") || response.headers.has("transfer-encoding") || !FetchResponse.isResponseWithBody(response.status);
if (request.method === "CONNECT" && !response.ok || request.method !== "CONNECT" && !isSelfDelimitingResponse) {
/**
* @note Defer the end-of-stream signal so the HTTP parser has a chance
* to process already-pushed response data and fire the 'response' event
* before the socket is ended. Without this, the parser marks the response
* as "complete" before the client can interact with it (e.g. `response.destroy()`).
*/
await new Promise((resolve) => process.nextTick(resolve));
socket.push(null);
}
}
#modifyHttpHeaders(request) {
const transformRequestMessage = (httpMessage, encoding) => {
/**
* @note Socket can write a buffer (e.g. uploaded file) even before
* it writes the HTTP message. Bypass those cases.
*/
if (encoding === "buffer") return httpMessage;
const parts = httpMessage.toString(encoding).split("\r\n");
const headersEndIndex = parts.findIndex((field) => field === "");
const httpMessageHeaderPairs = parts.slice(1, headersEndIndex);
const httpMessageRawHeaders = httpMessageHeaderPairs.map((line) => {
const separatorIndex = line.indexOf(": ");
return [line.slice(0, separatorIndex), line.slice(separatorIndex + 2)];
});
const requestRawHeaders = getRawFetchHeaders(request.headers);
if (httpMessageRawHeaders.length === requestRawHeaders.length && httpMessageRawHeaders.every((tuple, index) => {
const requestTuple = requestRawHeaders[index];
return tuple[0] === requestTuple[0] && tuple[1] === requestTuple[1];
})) return httpMessage;
const httpMessageHeaders = FetchResponse.parseRawHeaders(httpMessageHeaderPairs.flatMap((header) => header.split(": ")));
const visitedHeaders = /* @__PURE__ */ new Set();
for (const [headerName] of requestRawHeaders) {
const normalizedHeaderName = headerName.toLowerCase();
if (visitedHeaders.has(normalizedHeaderName)) continue;
visitedHeaders.add(normalizedHeaderName);
/**
* @note Forbidden Fetch headers (e.g. Host, Origin, Connection)
* are stripped from `request.headers` but remain in the raw
* headers list. Skip them so the original values from the
* outgoing HTTP message are preserved.
*/
const headerValue = request.headers.get(headerName);
if (headerValue === null) continue;
httpMessageHeaders.set(headerName, headerValue);
}
visitedHeaders.clear();
const httpMessageHeadersString = Array.from(httpMessageHeaders).map(([name, value]) => `${name}: ${value}`).join("\r\n");
parts.splice(1, headersEndIndex - 1, httpMessageHeadersString);
return parts.join("\r\n");
};
return (pendingData, encoding, callback) => {
if (Array.isArray(pendingData)) pendingData[0].chunk = transformRequestMessage(pendingData[0].chunk, pendingData[0].encoding);
else pendingData = transformRequestMessage(pendingData, encoding);
callback(pendingData);
};
}
};
//#endregion
export { requestContext as a, forwardHttpEvents as i, handleRequest as n, runInRequestContext as o, HttpResponseEvent as r, NodeHttpRequestSource as t };
//# sourceMappingURL=source-CIgPng7r.js.map

Sorry, the diff of this file is too big to display

+2
-2

@@ -1,4 +0,4 @@

import { i as forwardHttpEvents, o as runInRequestContext, t as NodeHttpRequestSource } from "../../source-BUVce4Q1.js";
import { i as forwardHttpEvents, o as runInRequestContext, t as NodeHttpRequestSource } from "../../source-CIgPng7r.js";
import { i as Interceptor } from "../../buffer-utils-BvPY1Tc-.js";
import { o as patchesRegistry } from "../../net-9sRKnjIG.js";
import { o as patchesRegistry } from "../../net-Ca8p1rIe.js";
import http from "node:http";

@@ -5,0 +5,0 @@ import https from "node:https";

@@ -1,6 +0,6 @@

import { a as requestContext, i as forwardHttpEvents, t as NodeHttpRequestSource } from "../../source-BUVce4Q1.js";
import { a as requestContext, i as forwardHttpEvents, t as NodeHttpRequestSource } from "../../source-CIgPng7r.js";
import { i as Interceptor } from "../../buffer-utils-BvPY1Tc-.js";
import { i as getErrorResponse } from "../../fetch-utils-Dw1PsmtX.js";
import { o as patchesRegistry } from "../../net-9sRKnjIG.js";
import { t as hasConfigurableGlobal } from "../../has-configurable-global-IskkJJOL.js";
import { o as patchesRegistry } from "../../net-Ca8p1rIe.js";
import { t as hasConfigurableGlobal } from "../../has-configurable-global-BU1sT1u4.js";
//#region src/interceptors/fetch/node.ts

@@ -7,0 +7,0 @@ /**

@@ -1,2 +0,2 @@

import { i as forwardHttpEvents, t as NodeHttpRequestSource } from "../../source-BUVce4Q1.js";
import { i as forwardHttpEvents, t as NodeHttpRequestSource } from "../../source-CIgPng7r.js";
import { i as Interceptor } from "../../buffer-utils-BvPY1Tc-.js";

@@ -3,0 +3,0 @@ //#region src/interceptors/http/index.ts

@@ -164,2 +164,14 @@ import { t as Interceptor } from "../../interceptor-dtJZ_9QT.js";

reset(connectionOptions?: NetworkConnectionOptions & net.SocketConnectOpts): void;
/**
* Schedule a reset of this controller to happen right before the
* next client write. The consumer signals the boundary of the
* current exchange (e.g. an HTTP message boundary) the moment it is
* parsed — mid-delivery of the exchange's final chunk — when an
* immediate reset would discard that chunk. Deferring the reset to
* the next write lets the current exchange settle as-is while the
* next exchange's first write already buffers for a new verdict
* (instead of following the previous one, e.g. leaking to the
* passthrough connection).
*/
scheduleReset(): void;
protected emulateConnect(): void;

@@ -166,0 +178,0 @@ /**

@@ -1,2 +0,2 @@

import { t as SocketInterceptor } from "../../net-9sRKnjIG.js";
import { t as SocketInterceptor } from "../../net-Ca8p1rIe.js";
export { SocketInterceptor };

@@ -1,6 +0,6 @@

import { a as requestContext, i as forwardHttpEvents, t as NodeHttpRequestSource } from "../../source-BUVce4Q1.js";
import { a as requestContext, i as forwardHttpEvents, t as NodeHttpRequestSource } from "../../source-CIgPng7r.js";
import { a as createLogger, i as Interceptor } from "../../buffer-utils-BvPY1Tc-.js";
import { t as FetchRequest } from "../../fetch-utils-Dw1PsmtX.js";
import { o as patchesRegistry } from "../../net-9sRKnjIG.js";
import { t as hasConfigurableGlobal } from "../../has-configurable-global-IskkJJOL.js";
import { o as patchesRegistry } from "../../net-Ca8p1rIe.js";
import { t as hasConfigurableGlobal } from "../../has-configurable-global-BU1sT1u4.js";
//#region src/interceptors/XMLHttpRequest/node.ts

@@ -7,0 +7,0 @@ const logger = createLogger("xhr");

@@ -1,2 +0,2 @@

import { n as handleRequest, r as HttpResponseEvent } from "./source-BUVce4Q1.js";
import { n as handleRequest, r as HttpResponseEvent } from "./source-CIgPng7r.js";
import { a as createLogger, i as Interceptor } from "./buffer-utils-BvPY1Tc-.js";

@@ -3,0 +3,0 @@ import { t as BatchInterceptor } from "./batch-interceptor-ByEZVih3.js";

@@ -5,3 +5,3 @@ {

"description": "Low-level network interception library for Node.js.",
"version": "0.42.2",
"version": "0.42.3",
"main": "./lib/node/index.js",

@@ -8,0 +8,0 @@ "types": "./lib/node/index.d.ts",

@@ -12,2 +12,3 @@ import { Readable } from 'node:stream'

onRequest: (request: Request, abortController: AbortController) => void
onMessageComplete?: () => void
}

@@ -17,6 +18,8 @@

#requestBodyStream?: Readable
#upgrade = false
constructor(options: HttpRequestParserOptions) {
super(1, {
onHeadersComplete: ({ rawHeaders, method, url: path }) => {
onHeadersComplete: ({ rawHeaders, method, url: path, upgrade }) => {
this.#upgrade = upgrade
/**

@@ -84,2 +87,11 @@ * @note When the socket is reused, "connectionOptions" will point

this.#requestBodyStream?.push(null)
/**
* @note An upgraded exchange (e.g. "CONNECT", WebSocket) has
* no message boundary: it takes the connection over, and the
* bytes that follow belong to it, not to a next exchange.
*/
if (!this.#upgrade) {
options.onMessageComplete?.()
}
},

@@ -86,0 +98,0 @@ })

@@ -171,2 +171,13 @@ import net from 'node:net'

},
/**
* @note The message boundary ends the current exchange.
* Schedule the controller reset so the next write on this
* (kept-alive) socket opens a new exchange and buffers for
* its own verdict instead of following the settled one
* (e.g. leaking a mocked request to the server of a
* previously passed-through exchange).
*/
onMessageComplete: () => {
socketController.scheduleReset()
},
onRequest: async (parsedRequest, requestAbortController) => {

@@ -173,0 +184,0 @@ const request =

@@ -481,2 +481,3 @@ import net from 'node:net'

#realHandleSwapped = false
#resetScheduled = false
#clientCloseEmitted = false

@@ -655,5 +656,21 @@ #clientEndPushed = false

/**
* Schedule a reset of this controller to happen right before the
* next client write. The consumer signals the boundary of the
* current exchange (e.g. an HTTP message boundary) the moment it is
* parsed — mid-delivery of the exchange's final chunk — when an
* immediate reset would discard that chunk. Deferring the reset to
* the next write lets the current exchange settle as-is while the
* next exchange's first write already buffers for a new verdict
* (instead of following the previous one, e.g. leaking to the
* passthrough connection).
*/
public scheduleReset(): void {
this.#resetScheduled = true
}
#reset(): void {
logger.verbose('resetting the socket...')
this.#resetScheduled = false
this.readyState = SocketController.PENDING

@@ -733,2 +750,12 @@ this.pendingConnection = Promise.withResolvers()

/**
* @note A scheduled reset marks the boundary of the previous
* exchange (see "scheduleReset"). This write is the first one past
* the boundary; reset before dispatching so it opens the next
* exchange instead of following the previous exchange's verdict.
*/
if (this.#resetScheduled) {
this.reset()
}
/**
* @note Buffer the write BEFORE pushing the data to the server socket.

@@ -735,0 +762,0 @@ * Handling the pushed data may transition this controller within the

import { a as getDeepPropertyDescriptor } from "./net-9sRKnjIG.js";
//#region src/utils/has-configurable-global.ts
/**
* Returns a boolean indicating whether the given global property
* is defined and is configurable.
*/
function hasConfigurableGlobal(propertyName) {
const match = getDeepPropertyDescriptor(globalThis, propertyName);
if (typeof match === "undefined") return false;
const { descriptor } = match;
if (typeof descriptor.get === "function" && typeof descriptor.get() === "undefined") return false;
if (typeof descriptor.get === "undefined" && descriptor.value == null) return false;
if (typeof descriptor.set === "undefined" && !descriptor.configurable) {
console.error(`[MSW] Failed to apply interceptor: the global \`${propertyName}\` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.`);
return false;
}
return true;
}
//#endregion
export { hasConfigurableGlobal as t };
//# sourceMappingURL=has-configurable-global-IskkJJOL.js.map
{"version":3,"file":"has-configurable-global-IskkJJOL.js","names":[],"sources":["../../src/utils/has-configurable-global.ts"],"sourcesContent":["import { getDeepPropertyDescriptor } from './patches-registry'\n\n/**\n * Returns a boolean indicating whether the given global property\n * is defined and is configurable.\n */\nexport function hasConfigurableGlobal(propertyName: string): boolean {\n const match = getDeepPropertyDescriptor(globalThis, propertyName)\n\n // The property is not set at all.\n if (typeof match === 'undefined') {\n return false\n }\n\n const { descriptor } = match\n\n // The property is set to a getter that returns undefined.\n if (\n typeof descriptor.get === 'function' &&\n typeof descriptor.get() === 'undefined'\n ) {\n return false\n }\n\n // The property is set to a value equal to undefined.\n if (typeof descriptor.get === 'undefined' && descriptor.value == null) {\n return false\n }\n\n if (typeof descriptor.set === 'undefined' && !descriptor.configurable) {\n console.error(\n `[MSW] Failed to apply interceptor: the global \\`${propertyName}\\` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.`\n )\n return false\n }\n\n return true\n}\n"],"mappings":";;;;;;AAMA,SAAgB,sBAAsB,cAA+B;CACnE,MAAM,QAAQ,0BAA0B,YAAY,YAAY;CAGhE,IAAI,OAAO,UAAU,aACnB,OAAO;CAGT,MAAM,EAAE,eAAe;CAGvB,IACE,OAAO,WAAW,QAAQ,cAC1B,OAAO,WAAW,IAAI,MAAM,aAE5B,OAAO;CAIT,IAAI,OAAO,WAAW,QAAQ,eAAe,WAAW,SAAS,MAC/D,OAAO;CAGT,IAAI,OAAO,WAAW,QAAQ,eAAe,CAAC,WAAW,cAAc;EACrE,QAAQ,MACN,mDAAmD,aAAa,mKAClE;EACA,OAAO;CACT;CAEA,OAAO;AACT"}
import { a as createLogger, i as Interceptor, r as toBuffer } from "./buffer-utils-BvPY1Tc-.js";
import { TypedEvent } from "rettime";
import { invariant } from "outvariant";
import http from "node:http";
import net from "node:net";
import tls from "node:tls";
//#region src/utils/patches-registry.ts
var PatchesRegistry = class {
#replacements = /* @__PURE__ */ new Map();
applyPatch(owner, key, getNextValue) {
const ownerReplacements = this.#replacements.get(owner);
invariant(!ownerReplacements?.has(key), `Failed to replace a global value at "${String(key)}": already replaced.`);
const match = getDeepPropertyDescriptor(owner, key);
if (typeof match === "undefined") {
console.warn(`Failed to replace a global value at "${String(key)}": not a global value.`);
return () => {};
}
if (match.descriptor.configurable) Object.defineProperty(owner, key, {
value: getNextValue(owner[key]),
enumerable: true,
configurable: true
});
else if (match.descriptor.writable) owner[key] = getNextValue(owner[key]);
else throw new Error(`Failed to patch a non-configurable non-writable property "${key.toString()}"`);
const restorePatch = () => {
const currentReplacements = this.#replacements.get(owner);
if (!currentReplacements?.has(key)) return;
if (match.owner === owner)
/**
* @note Restoring non-configurable properties works as long as "writable: true"
* and none of the other descriptor properties except for "value" have changed.
*/
Object.defineProperty(match.owner, key, match.descriptor);
else
/**
* @todo Delete the proxy property set by the registry.
* If the match's owner isn't the original owner, the property is likely nested in the prototype.
* The registry does not meddle with those, they are left intact.
*/
Reflect.deleteProperty(owner, key);
currentReplacements.delete(key);
if (currentReplacements.size === 0) this.#replacements.delete(owner);
};
if (ownerReplacements) ownerReplacements.set(key, restorePatch);
else this.#replacements.set(owner, /* @__PURE__ */ new Map([[key, restorePatch]]));
return restorePatch;
}
restoreAllPatches() {
const errors = [];
for (const [, ownerReplacements] of this.#replacements) for (const [, restorePatch] of ownerReplacements) try {
restorePatch();
} catch (error) {
if (error instanceof Error) errors.push(error);
else throw error;
}
if (errors.length > 0) throw new AggregateError(errors, "FOO!");
}
};
const patchesRegistry = new PatchesRegistry();
/**
* Returns a property descriptor for the given property on the owner.
* Walks down the prototype chain if the property does not exist on the owner.
* Handy for getting a global property descriptor where `globalThis` is
* replaced with a controlled class (e.g. ServiceWorkerGlobalScope).
*/
function getDeepPropertyDescriptor(owner, key) {
let currentOwner = owner;
let descriptor;
while (currentOwner) {
descriptor = Object.getOwnPropertyDescriptor(currentOwner, key);
if (descriptor) return {
owner: currentOwner,
descriptor
};
currentOwner = Object.getPrototypeOf(currentOwner);
}
}
//#endregion
//#region src/interceptors/net/utils/normalize-net-connect-args.ts
/**
* Normalizes the arguments passed to `net.connect()`.
*/
function normalizeNetConnectArgs(args) {
if (args.length === 0) return [{ path: "" }, null];
const callback = typeof args[1] === "function" ? args[1] : args[2] || null;
if (typeof args[0] === "string") return [{ path: args[0] }, callback];
if (typeof args[0] === "number") return [{
port: args[0],
path: "",
/**
* @note The host is optional in the "(port[, host][, callback])"
* signatures and defaults to "localhost" in Node.js.
*/
host: typeof args[1] === "string" ? args[1] : void 0
}, callback];
if (typeof args[0] === "object") {
/**
* @note URL arguments receive no special treatment on purpose.
* Node.js does not support them and reads a given URL as a plain
* options object (e.g. "url.port" is a string, "url.host" includes
* the port). The "port" branch below reproduces that reading.
*/
if ("port" in args[0]) return [{
path: "",
port: Reflect.get(args[0], "port"),
host: Reflect.get(args[0], "host"),
auth: Reflect.get(args[0], "auth"),
family: Reflect.get(args[0], "family"),
hints: Reflect.get(args[0], "hints"),
session: Reflect.get(args[0], "session"),
localAddress: Reflect.get(args[0], "localAddress"),
localPort: Reflect.get(args[0], "localPort"),
timeout: Reflect.get(args[0], "timeout"),
lookup: Reflect.get(args[0], "lookup"),
allowHalfOpen: Reflect.get(args[0], "allowHalfOpen"),
noDelay: Reflect.get(args[0], "noDelay"),
keepAlive: Reflect.get(args[0], "keepAlive"),
keepAliveInitialDelay: Reflect.get(args[0], "keepAliveInitialDelay"),
autoSelectFamily: Reflect.get(args[0], "autoSelectFamily"),
autoSelectFamilyAttemptTimeout: Reflect.get(args[0], "autoSelectFamilyAttemptTimeout")
}, callback];
return [{
path: args[0].path || "",
family: Reflect.get(args[0], "family"),
session: Reflect.get(args[0], "session"),
auth: Reflect.get(args[0], "auth"),
timeout: Reflect.get(args[0], "timeout"),
allowHalfOpen: Reflect.get(args[0], "allowHalfOpen")
}, callback];
}
throw new Error(`Invalid arguments passed to net.connect: ${args}`);
}
//#endregion
//#region src/interceptors/net/utils/flush-writes.ts
/**
* Write the given pending data to the socket using its public
* "write()" method. Unlike direct "_writeGeneric()" calls, public
* writes go through the "Writable" queue that only dispatches the
* next write once the previous one completes. TLS sockets rely on
* that serialization: their "TLSWrap" handle supports a single
* write in flight and crashes the process with a native
* "Assertion failed: !current_write_" abort when writes overlap
* (e.g. multiple direct "_writeGeneric()" calls issued on a
* connecting socket replay back-to-back on "connect").
*/
function writePendingData(socket, data, encoding, callback) {
if (Array.isArray(data)) {
for (let index = 0; index < data.length; index++) {
const entry = data[index];
/**
* @note Per-entry callbacks are intentionally ignored, the same
* way "writevGeneric()" only honors the batch-level callback.
*/
if (index === data.length - 1) socket.write(entry.chunk, entry.encoding, callback);
else socket.write(entry.chunk, entry.encoding);
}
return;
}
socket.write(data, encoding, callback);
}
function unwrapPendingData(data, callback) {
if (Array.isArray(data)) for (const entry of data) callback(entry.chunk, entry.encoding, entry.callback);
else callback(data);
}
//#endregion
//#region src/interceptors/net/utils/get-tls-connect-options.ts
/**
* Returns the original options the given TLS socket was created with.
* The original `tls.connect()` stores them on the socket instance
* under an internal symbol before connecting its transport.
* @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1690
*/
function getTlsConnectOptions(socket) {
const kConnectOptions = Object.getOwnPropertySymbols(socket).find((symbol) => {
return symbol.description === "connect-options";
});
if (kConnectOptions == null) return;
return Reflect.get(socket, kConnectOptions);
}
//#endregion
//#region src/interceptors/net/utils/address-info.ts
function getAddressInfoByConnectionOptions(options) {
if (options == null) return {};
const isIPv6 = options.family === 6 || net.isIPv6(options.host || "");
return {
address: isIPv6 ? "::1" : "127.0.0.1",
/**
* @note Coerce the port to a number. Connection options may
* describe it as a string (e.g. when created from a URL), while
* the address info always reports a numeric port.
*/
port: Number(options.port) || (options.protocol === "https:" ? 443 : 80),
family: isIPv6 ? "IPv6" : "IPv4"
};
}
/**
* Get the local address info for the given connection options.
* This describes the client-side end of the connection: the socket
* is bound to the loopback interface and an ephemeral port, the same
* way the operating system binds an outgoing connection.
*/
function getLocalAddressInfoByConnectionOptions(options) {
if (options == null) return {};
const isIPv6 = options.family === 6 || net.isIPv6(options.host || "");
return {
address: options.localAddress || (isIPv6 ? "::1" : "127.0.0.1"),
port: options.localPort || getEphemeralPort(),
family: isIPv6 ? "IPv6" : "IPv4"
};
}
/**
* Get a random port from the ephemeral range (IANA: 49152-65535),
* the range the operating system draws from when binding an
* outgoing connection.
*/
function getEphemeralPort() {
return 49152 + Math.floor(Math.random() * 16384);
}
//#endregion
//#region src/interceptors/net/socket-controller.ts
const kListenerWrap = Symbol("kListenerWrap");
const kRawSocket = Symbol("kRawSocket");
const kPatched = Symbol("kPatched");
const logger$1 = createLogger("socket");
function toServerSocket(socket) {
/**
* The server-side write buffer. Pushing data to the client honors
* the client's read backpressure: once "socket.push()" reports a
* full buffer, subsequent server writes queue here and flush when
* the client reads again ("_read"). This mirrors how a real server
* cannot write faster than the client consumes.
*/
const pendingWrites = [];
let pendingEnd;
let isBackpressured = false;
let isFlushScheduled = false;
const flushPendingWrites = () => {
/**
* @note The mocked connection is not established yet. Flush once
* it is, the same way a real server cannot write to a client
* that has not connected.
*/
if (socket.connecting) {
isBackpressured = true;
if (!isFlushScheduled) {
isFlushScheduled = true;
socket.once("ready", () => {
isFlushScheduled = false;
flushPendingWrites();
});
}
return false;
}
while (pendingWrites.length > 0) {
const nextWrite = pendingWrites.shift();
socket._unrefTimer();
const canPushMore = socket.push(toBuffer(nextWrite.chunk, nextWrite.encoding), nextWrite.encoding);
nextWrite.callback?.();
if (!canPushMore) {
isBackpressured = true;
return false;
}
}
const wasBackpressured = isBackpressured;
isBackpressured = false;
if (pendingEnd) {
const finalEnd = pendingEnd;
pendingEnd = void 0;
if (finalEnd.chunk != null) socket.push(toBuffer(finalEnd.chunk, finalEnd.encoding), finalEnd.encoding);
socket.push(null);
finalEnd.callback?.();
}
if (wasBackpressured) socket.emit("internal:drain");
return true;
};
/**
* @note "_read" is the client asking for more data. Flush the
* buffered server writes so the delivery resumes as the client
* reads, completing the backpressure loop.
*/
const realRead = socket._read.bind(socket);
socket._read = (size) => {
realRead(size);
if (pendingWrites.length > 0 || pendingEnd != null || isBackpressured) flushPendingWrites();
};
return new Proxy(socket, { get: (target, property, receiver) => {
const getRealValue = () => {
return Reflect.get(target, property, receiver);
};
if (property === "on" || property === "addListener" || property === "once" || property === "prependListener" || property === "prependOnceListener") {
const realAddListener = getRealValue();
return (event, listener) => {
if (event === "data") {
const listenerWrap = (chunk, encoding) => {
listener(toBuffer(chunk, encoding));
};
Object.defineProperty(listener, kListenerWrap, {
enumerable: false,
writable: false,
value: listenerWrap
});
/**
* @note Subscribe using the same method (e.g. "once") so its
* listener semantics apply to the internal channel too.
*/
Reflect.apply(realAddListener, target, ["internal:write", listenerWrap]);
return target;
}
/**
* @note The "drain" event on the server socket signals the
* flush of the server-side write buffer. It is routed through
* an internal channel so it does not clash with the "drain"
* event of the underlying client socket.
*/
if (event === "drain") {
Reflect.apply(realAddListener, target, ["internal:drain", listener]);
return target;
}
return realAddListener.call(target, event, listener);
};
}
if (property === "off" || property === "removeListener") {
const realRemoveListener = getRealValue();
return (event, listener) => {
if (event === "data") {
const listenerWrap = listener[kListenerWrap];
if (listenerWrap) return realRemoveListener.call(target, "internal:write", listenerWrap);
}
if (event === "drain") return realRemoveListener.call(target, "internal:drain", listener);
return realRemoveListener.call(target, event, listener);
};
}
if (property === "write") return ((chunk, encoding, callback) => {
if (typeof encoding === "function") {
callback = encoding;
encoding = void 0;
}
pendingWrites.push({
chunk,
encoding,
callback
});
/**
* @note Do not push more data to a client that is not
* reading. The write stays buffered until the client reads
* again, and "drain" signals the flush.
*/
if (isBackpressured) return false;
return flushPendingWrites();
});
if (property === "end") return ((...args) => {
const callback = args[args.length - 1];
/**
* @note The end-of-stream is delivered once the buffered
* writes flush so the client never observes the EOF before
* the data that preceded it.
*/
pendingEnd = {
chunk: typeof args[0] === "function" ? void 0 : args[0],
encoding: typeof args[1] === "string" ? args[1] : void 0,
callback: typeof callback === "function" ? callback : void 0
};
flushPendingWrites();
/**
* @note Do not end the client socket's writable side.
* The server ending the connection only signals EOF to the
* client (the client may keep writing on half-open sockets).
*/
return target;
});
return getRealValue();
} });
}
var SocketController = class SocketController {
static {
this.PENDING = 0;
}
static {
this.CLAIMED = 1;
}
static {
this.PASSTHROUGH = 2;
}
#awaitedVerdicts = 0;
constructor(socket) {
this[kRawSocket] = socket;
socket[kPatched] = true;
this.readyState = SocketController.PENDING;
}
/**
* Claim this socket. Once claimed, the connection attempt succeeds
* regardless of the requested host and the interceptor becomes the
* mocked server for this connection.
*/
claim() {
invariant(this.readyState === SocketController.PENDING, "Failed to claim a socket connection: already handled (%s)", this.readyState);
this.readyState = SocketController.CLAIMED;
}
/**
* Establish this socket connection as-is.
*/
passthrough() {
invariant(this.readyState === SocketController.PENDING, "Failed to passthrough a socket connection: already handled (%s)", this.readyState);
this.readyState = SocketController.PASSTHROUGH;
}
/**
* Await a verdict on this connection from the given number of
* subscribers. A connection nobody awaits to inspect (or one that
* every awaited subscriber has declined) is passed through as-is.
* This makes "unclaimed after everyone declined" a state owned by
* the controller instead of the individual subscribers.
*/
awaitVerdicts(count) {
this.#awaitedVerdicts = count;
if (this.#awaitedVerdicts === 0) this.passthrough();
}
/**
* Decline this socket connection. Declining means the subscriber
* has inspected the connection and will not handle it (e.g. the
* traffic is not of the protocol that subscriber implements).
* Once every awaited subscriber declines, the connection is
* passed through as-is.
*/
decline() {
if (this.readyState !== SocketController.PENDING) return;
this.#awaitedVerdicts -= 1;
if (this.#awaitedVerdicts <= 0)
/**
* @note Defer the passthrough so it never transitions this
* controller in the middle of a client write. Declines are
* issued while the written data is being pushed to the server
* socket, and a synchronous transition would race the write's
* own bookkeeping (e.g. re-buffering the write after a reset
* at an exchange boundary).
*/
process.nextTick(() => {
if (this.readyState === SocketController.PENDING && !this[kRawSocket].destroyed) this.passthrough();
});
}
};
var TcpSocketController = class extends SocketController {
#connectionOptions;
#retargetedConnectionOptions;
#realWriteGeneric;
#passthroughSocket = null;
#bufferedWrites = [];
#readsCorked = false;
#corkedReads = [];
#realHandleSwapped = false;
#clientCloseEmitted = false;
#clientEndPushed = false;
#connectEmulated = false;
constructor(socket, createConnection, connectionOptions) {
super(socket);
this.socket = socket;
this.createConnection = createConnection;
/**
* @note Plain TCP sockets capture the connection options from the
* "socket.connect()" proxy below. TLS sockets carry additional
* TLS-level options that never pass through "socket.connect()",
* so those are provided explicitly (see "TlsSocketController").
*/
this.#connectionOptions = connectionOptions;
this.socket._read = () => {
/**
* @note Resume the passthrough socket when the consumer asks for
* more data. Node.js calls "_read()" once the consumer drains the
* read buffer (e.g. after "resume()"). The passthrough socket may
* have been paused when the consumer's buffer got full, and this
* is the only signal to continue reading.
*/
this.#passthroughSocket?.resume();
};
this.#realWriteGeneric = this.socket._writeGeneric;
this.#bufferedWrites = [];
this.socket._writeGeneric = (...args) => {
this.#handleWrite(args);
};
this.socket.connect = new Proxy(this.socket.connect, { apply: (target, thisArg, args) => {
logger$1.verbose("socket.connect() %o", args);
this.#connectionOptions = args[0];
/**
* @note Do not bind the intercepted socket to the requested
* local address/port. Binding reserves the port for real, and
* the passthrough connection (created with the original options)
* would then bind the same port again, resulting in a conflict.
* The requested values are still reflected in the address info
* of a claimed socket (see "claim()").
*/
if (args[0] != null && typeof args[0] === "object" && (args[0].localAddress != null || args[0].localPort != null)) args[0] = {
...args[0],
localAddress: void 0,
localPort: void 0
};
return Reflect.apply(target, thisArg, args);
} });
/**
* @note A single socket can be reused for connections to the same host.
* When one connection ends, the Agent frees the socket, then uses it
* to write the next request's HTTP message immediately. Use the "free"
* event to transition the controller into the pending state so the
* next exchange is handled anew.
*/
socket.on("free", () => {
logger$1.verbose("client socket freed!");
this.reset();
}).on("close", () => {
logger$1.verbose("client socket closed!");
this.#clientCloseEmitted = true;
/**
* @note Destroy the passthrough socket, if any. The client
* socket is done, and a passthrough connection left open would
* linger until the server closes it (or error unhandled if it
* is still connecting).
*/
this.#passthroughSocket?.destroy();
this.#passthroughSocket = null;
this.#bufferedWrites = [];
this.#readsCorked = false;
this.#corkedReads = [];
});
this.serverSocket = toServerSocket(this.socket);
this.pendingConnection = Promise.withResolvers();
this.#reset();
}
/**
* Reset this controller to the pending state so the next exchange
* on this socket can be handled anew. This is meant for kept-alive
* sockets that are reused for multiple exchanges by clients that
* don't emit the "free" event on the socket (e.g. Undici).
*
* Providing connection options retargets this connection: the
* exchanges that follow belong to the given target (e.g. the
* authority of an established "CONNECT" tunnel). An unclaimed
* exchange then passes through to that target instead of the
* originally dialed one, and a claimed exchange reports it as the
* peer. The verdict count is deliberately not re-armed: subscribers
* that declined this connection's protocol stay declined across
* the exchanges, retargeted or not.
*/
reset(connectionOptions) {
if (connectionOptions != null) {
this.#retargetedConnectionOptions = connectionOptions;
this.#connectionOptions = connectionOptions;
/**
* @note The passthrough connection to the original target, if
* any, cannot serve the retargeted exchanges. Detach its
* forwarding listeners before destroying it so its teardown is
* not mistaken for the client connection's own (e.g. its "close"
* must not close the client socket).
*/
if (this.#passthroughSocket) {
this.#passthroughSocket.removeListener("connect", this.#onRealSocketConnect).removeListener("connectionAttemptFailed", this.#onRealSocketConnectionAttemptFailed).removeListener("connectionAttemptTimeout", this.#onRealSocketConnectionAttemptTimeout).removeListener("data", this.#onRealSocketData).removeListener("error", this.#onRealSocketError).removeListener("end", this.#onRealSocketEnd).removeListener("close", this.#onRealSocketClose).destroy();
this.#passthroughSocket = null;
/**
* @note The handle swapped in from the destroyed connection,
* if any, no longer carries this socket's traffic. Treat the
* socket as not swapped so the retargeted passthrough writes
* directly to the new connection until its own handle swap.
*/
this.#realHandleSwapped = false;
}
}
/**
* @note Only settled (claimed or passed-through) sockets need a reset.
* Resetting a pending socket again would discard the writes already
* buffered for the next exchange (e.g. the Agent "free" event firing
* after the parser has reset this controller at a message boundary).
*/
if (this.readyState === SocketController.PENDING) return;
this.#reset();
}
#reset() {
logger$1.verbose("resetting the socket...");
this.readyState = SocketController.PENDING;
this.pendingConnection = Promise.withResolvers();
this.#bufferedWrites = [];
this.#connectEmulated = false;
this.socket._pendingData = null;
this.socket._pendingEncoding = "";
const wrapHandle = (handle) => {
this.pendingConnection.promise.then(() => {
logger$1.verbose("connection request resolved!", this.readyState);
process.nextTick(() => {
/**
* @note If by this point the socket hasn't been handled,
* is still connecting, doesn't have any writes buffered,
* and has a "connect" listener, assume it's the "write after connect"
* scenario (e.g. undici). In that case, auto-claim the socket to
* transition to the connected state appropriately to its handle.
*/
if (this.readyState === SocketController.PENDING && this.socket.connecting && this.#bufferedWrites.length === 0 && this.socket.listenerCount("connect") > 0) {
logger$1.verbose("assume connect->write socket, calling \"connect\" listeners...");
this.emulateConnect();
}
});
});
/**
* Remove the "setTypeOfService" from the handle, if present (Node.js v24+).
* Removing it has no effect on the socket but prevents the "setTypeOfService EBADF" error.
* @see https://github.com/nodejs/node/blob/69a970f76814d40f55cf162d0cc3632fe8a7e599/lib/net.js#L661
* @see https://github.com/nodejs/undici/blob/bf684f7de01616708a33a5d1c092177622394442/lib/dispatcher/client-h1.js#L1136
*/
if (handle.setTypeOfService) handle.setTypeOfService = void 0;
handle.connect = handle.connect6 = (request) => {
logger$1.verbose("handle.connect()");
this.pendingConnection.resolve([request, handle]);
};
logger$1.verbose("socket handle wrapped! waiting for connection request...");
};
if (this.socket._handle) wrapHandle(this.socket._handle);
else this.socket.prependOnceListener("connectionAttempt", () => {
wrapHandle(this.socket._handle);
});
}
/**
* Handle a write performed on the client socket. Installed once as
* "_writeGeneric", this is the single write path for every controller
* state, dispatching on "readyState".
*/
#handleWrite(args) {
const data = args[1];
logger$1.verbose("socket write (state: %d) %o", this.readyState, args);
/**
* @note Buffer the write BEFORE pushing the data to the server socket.
* Handling the pushed data may transition this controller within the
* same call stack, and each transition settles the buffered entry:
* "passthrough()" flushes it to the real socket, "claim()" drops it,
* and a reset at an HTTP message boundary keeps it for the next exchange.
*/
this.#bufferedWrites.push(args);
if (this.readyState === SocketController.PENDING) {
/**
* @note Reflect the buffered write in "_pendingData" so it counts
* toward "bytesWritten", the same way Node.js counts the pending
* data of a connecting socket. Flushing the buffered writes resets
* "_pendingData" (see `passthrough()`).
*/
const pendingData = Array.isArray(this.socket._pendingData) ? this.socket._pendingData : [];
unwrapPendingData(data, (chunk, chunkEncoding) => {
pendingData.push({
chunk,
encoding: chunkEncoding
});
});
this.socket._pendingData = pendingData;
if (this.socket.listenerCount("internal:write") === 0) {
logger$1.verbose("no server data listeners, scheduling to the next tick...");
process.nextTick(() => {
this.#push(data);
});
} else this.#push(data);
} else this.#push(data);
/**
* Dispatch on the state observed AFTER the push since handling the
* pushed data may have transitioned this controller synchronously.
*/
switch (this.readyState) {
case SocketController.PENDING:
/**
* @note The write either awaits the verdict on this exchange or,
* if the push reset this controller at a message boundary, opens
* the next exchange (the reset cleared the buffer; re-buffer it).
*/
if (!this.#bufferedWrites.includes(args)) this.#bufferedWrites.push(args);
this.#acknowledgeWrite(args);
return;
case SocketController.CLAIMED:
/**
* @note Once claimed, there's nowhere else to write chunks to.
* The data was delivered to the server socket; complete the write
* so the socket's writable state settles (enabling "finish").
*/
this.#removeBufferedWrite(args);
this.#acknowledgeWrite(args);
return;
case SocketController.PASSTHROUGH:
/**
* @note A synchronous "passthrough()" has already flushed this
* write (with its callback) to the real socket.
*/
if (!this.#removeBufferedWrite(args)) return;
/**
* @note Until the handle swap, the client socket's own handle
* cannot carry any data (it never actually connects). Write
* directly to the passthrough socket instead. This also prevents
* the "connecting" write replay of `Socket.prototype._writeGeneric`
* from pushing the same data to the server socket twice.
*/
if (!this.#realHandleSwapped && this.#passthroughSocket) {
writePendingData(this.#passthroughSocket, data, args[2], args[3]);
return;
}
this.#realWriteGeneric.apply(this.socket, args);
}
}
/**
* Complete the given write by invoking its callback. The callback is
* then removed from the write entry so flushing that entry later
* (e.g. on passthrough) does not invoke it twice.
*/
#acknowledgeWrite(args) {
const callback = args[3];
if (typeof callback === "function") {
callback();
args[3] = void 0;
}
}
#removeBufferedWrite(args) {
const index = this.#bufferedWrites.indexOf(args);
if (index === -1) return false;
this.#bufferedWrites.splice(index, 1);
return true;
}
emulateConnect() {
this.#connectEmulated = true;
/**
* @note Reflect the connected state before notifying the listeners,
* the same way Node.js does before emitting "connect".
*/
Reflect.set(this.socket, "connecting", false);
/**
* @note Invoke the raw listeners so the "once" wrappers get
* consumed. This prevents listeners like the connection callback
* from being invoked again when "connect" is emitted for real
* (e.g. once the claimed connection completes).
*/
for (const listener of this.socket.rawListeners("connect")) listener.apply(this.socket);
}
/**
* Push the given data to the server socket.
* This has no effect on the public-facing socket and is used
* only for the interceptors to subscribe to "socket.on('data')"
* before the data is actually written anywhere.
*/
#push = (data) => {
if (data == null) return;
logger$1.verbose("server push %o", data);
unwrapPendingData(data, (chunk, encoding) => {
logger$1.verbose("server emitting \"data\" %o", {
chunk,
encoding
});
this.socket.emit("internal:write", chunk, encoding);
});
};
#onRealSocketConnect = () => {
if (!this.#passthroughSocket) return;
/**
* @note The consumer may destroy the socket while the passthrough
* connection is still being established. The passthrough "connect"
* (I/O poll phase) can arrive before the client's "close" callback
* (close phase) within the same event loop iteration. A destroyed
* socket must not emit "connect"/"ready".
*/
if (this.socket.destroyed) {
this.#passthroughSocket.destroy();
return;
}
const replacedHandle = this.socket._handle;
const wasUnrefed = replacedHandle != null && typeof replacedHandle.hasRef === "function" && !replacedHandle.hasRef();
this.socket._handle = this.#passthroughSocket._handle;
this.#realHandleSwapped = true;
/**
* @note Preserve the ref state across the handle swap. If the
* consumer unrefed the socket while it was connecting, the swapped
* handle must not hold the process alive either.
*/
if (wasUnrefed) this.socket._handle.unref?.();
/**
* @note Close the replaced handle. Nothing references it past this
* point, and left open, it keeps the process alive indefinitely.
* For TLS sockets, the replaced handle is a TLSWrap; close its
* underlying transport too (closing the wrap alone does not close
* the TCP handle it sits on).
*/
if (replacedHandle != null) {
replacedHandle.close();
replacedHandle._parent?.close();
}
Reflect.set(this.socket, "connecting", false);
/**
* @note Read the remote address info once so it gets cached on the
* socket. The passthrough socket controls the swapped handle and may
* close it at any point (e.g. once the server ends the connection),
* while Node.js keeps serving the cached info for sockets that
* have connected. Reading must happen after the socket is no longer
* connecting (the info of connecting sockets is never cached).
*/
this.socket.remoteAddress;
this.socket.emit("connect");
this.socket.emit("ready");
};
#onRealSocketConnectionAttemptFailed = (address, port, family, error) => {
this.socket.emit("connectionAttemptFailed", address, port, family, error);
};
#onRealSocketConnectionAttemptTimeout = (address, port, family) => {
this.socket.emit("connectionAttemptTimeout", address, port, family);
};
#onRealSocketData = (data) => {
logger$1.verbose("real socket \"data\" event %o", data);
/**
* @note Receiving data is socket activity. Refresh the idle timer
* of the client socket the same way its own reads would
* ("socket.setTimeout()" must not fire during an active transfer).
* The data arrives on the real socket, which only refreshes the
* real socket's timer.
*/
this.socket._unrefTimer();
if (this.#readsCorked) {
logger$1.verbose("reads are corked, buffering the data...");
this.#corkedReads.push({
type: "data",
chunk: data
});
return;
}
if (!this.socket.push(data)) {
logger$1.verbose("client socket forbade more pushes, pausing the passthrough socket...");
this.#passthroughSocket?.pause();
}
};
#onRealSocketError = (error) => {
logger$1.verbose("real socket \"error\" event %o", error);
if (this.socket.destroyed) {
logger$1.verbose("real socket errored but client socket already destroyed, skipping...");
return;
}
logger$1.verbose("real socket errored, forwarding %o", error);
this.socket.destroy(error);
if (this.#realHandleSwapped) process.nextTick(() => this.socket.emit("close", true));
};
#onRealSocketEnd = () => {
this.socket._unrefTimer();
if (this.#readsCorked) {
this.#corkedReads.push({ type: "end" });
return;
}
this.#clientEndPushed = true;
this.socket.push(null);
};
#onRealSocketClose = (hadError) => {
/**
* @note The connection is fully closed and the swapped handle
* cannot shut down anymore. Report a synchronous shutdown so the
* automatic "end()" of a half-closed socket ("allowHalfOpen: false")
* finishes without errors once the consumer reads the end-of-stream.
*/
if (this.#realHandleSwapped && this.socket._handle) this.socket._handle.shutdown = () => 1;
if (this.#readsCorked) {
this.#corkedReads.push({
type: "close",
hadError
});
return;
}
if (this.#clientCloseEmitted) return;
if (this.socket.destroyed && !this.#realHandleSwapped) return;
this.#emitClientClose(hadError);
};
/**
* Emit the "close" event on the client socket, honoring the order
* of the socket teardown events.
* @note The client may be paused with the received data (and the
* end-of-stream) still buffered. Node.js never emits "close" before
* "end" on a gracefully closed connection: the teardown waits until
* the consumer reads the buffered data.
*/
#emitClientClose(hadError) {
if (this.#clientEndPushed && !this.socket.readableEnded && !this.socket.destroyed) {
let closeDelivered = false;
const deliverClose = (hadErrorOverride) => {
if (closeDelivered) return;
closeDelivered = true;
process.nextTick(() => {
if (!this.#clientCloseEmitted) this.socket.emit("close", hadErrorOverride ?? hadError);
});
};
this.socket.once("end", () => {
deliverClose();
});
/**
* @note The consumer may also destroy the socket before reading
* the buffered data. Node.js still emits "close" for such
* sockets, but the destroy machinery of a socket with a swapped
* (already closed) handle never completes. Deliver "close" here.
*/
const realDestroy = this.socket._destroy;
this.socket._destroy = (error, callback) => {
deliverClose(error != null);
return realDestroy.call(this.socket, error, callback);
};
return;
}
this.socket.emit("close", hadError);
}
#onMockSocketDrain = () => {
logger$1.verbose("client socket drained!");
this.#passthroughSocket?.resume();
};
/**
* Forward the client's half-close to the passthrough socket.
* The client's writable side may finish before the real handle is
* swapped in ("_final" of a connected client runs against the mock
* handle), leaving the FIN unsent. Once the handle is swapped, the
* client's own shutdown reaches the real connection natively.
*/
#forwardClientFinish = () => {
if (!this.#realHandleSwapped) this.#passthroughSocket?.end();
};
/**
* Suspend forwarding of the passthrough socket events ("data", "end", "close")
* to the client socket. The events are buffered in order until `uncorkReads()`
* is called. This allows the consumer to delay the delivery of the original
* response to the client (e.g. until its own asynchronous logic settles).
*
* @note Pausing the client socket is not enough to delay the delivery.
* Consumers like Undici read the pushed data from a paused socket
* directly via `socket.read()`, which is unaffected by `socket.pause()`.
*/
corkReads() {
this.#readsCorked = true;
}
/**
* Resume forwarding of the passthrough socket events to the client socket,
* replaying any events buffered while the reads were corked.
*/
uncorkReads() {
if (!this.#readsCorked) return;
this.#readsCorked = false;
for (const corkedRead of this.#corkedReads.splice(0)) switch (corkedRead.type) {
case "data":
if (!this.socket.push(corkedRead.chunk)) {
logger$1.verbose("client socket forbade more pushes, pausing the passthrough socket...");
this.#passthroughSocket?.pause();
}
break;
case "end":
this.#clientEndPushed = true;
this.socket.push(null);
break;
case "close":
this.#emitClientClose(corkedRead.hadError);
break;
}
}
claim() {
super.claim();
/**
* @note The client may destroy the socket before the connection
* is claimed (e.g. abort a request in-flight). There is no
* connection to mock then, and the destroyed socket has no handle.
*/
if (this.socket.destroyed) {
logger$1.verbose("socket already destroyed, skipping claim...");
return;
}
/**
* @note Skip already connected sockets (e.g. kept-alive sockets
* reused for the next exchange). Sockets with an emulated "connect"
* only appear connected and must still complete the mock connection.
*/
if (!this.socket.connecting && !this.#connectEmulated) {
logger$1.verbose("socket already connected, skipping claim...");
return;
}
logger$1.verbose("-> claim!");
/**
* @note Reflect the local end of the claimed socket, the same way
* the operating system reports the bound address of an outgoing
* connection via "socket.address()"/"localAddress"/"localPort".
* Patching the handle also prevents Node.js from handling the
* "getsockname" errors of the never-connected raw handle.
* @see https://github.com/nodejs/node/blob/13eb80f3b718452213e0fc449702aefbbfe4110f/lib/net.js#L971
*/
this.socket._handle.getsockname = (addressInfo) => {
Object.assign(addressInfo, getLocalAddressInfoByConnectionOptions(this.#connectionOptions));
return 0;
};
/**
* @note Reflect the connection target as the peer of the claimed
* socket, the same way a connected socket reports the server it
* connected to via "remoteAddress"/"remotePort"/"remoteFamily".
*/
this.socket._handle.getpeername = (addressInfo) => {
Object.assign(addressInfo, getAddressInfoByConnectionOptions(this.#connectionOptions));
return 0;
};
this.#bufferedWrites = [];
this.socket._pendingData = null;
this.socket._pendingEncoding = "";
this.pendingConnection.promise.then(([request, handle]) => {
logger$1.verbose("connection request resolved, mocking the connection...");
/**
* @note "afterConnect" asserts that the socket is connecting.
* Restore the flag if the connect was emulated earlier (emulation
* flips it so its listeners observe a connected socket).
* "afterConnect" itself sets it back to false.
*/
if (this.#connectEmulated) Reflect.set(this.socket, "connecting", true);
/**
* @see https://github.com/nodejs/node/blob/9cd6630870b776e96c5cf0ac68c31e2f46df3835/lib/net.js#L1142
*/
request.oncomplete(0, handle, request, true, true);
});
}
passthrough(flushPendingData) {
super.passthrough();
logger$1.verbose("-> passthrough!");
const createRealSocket = () => {
const realSocket = this.#retargetedConnectionOptions ? this.#createRetargetedConnection(this.#retargetedConnectionOptions) : this.createConnection();
realSocket[kPatched] = true;
if (this.socket.timeout != null) realSocket.setTimeout(this.socket.timeout);
return realSocket;
};
const realSocket = this.#passthroughSocket && !this.#passthroughSocket.destroyed ? this.#passthroughSocket : createRealSocket();
if (realSocket !== this.#passthroughSocket) this.#passthroughSocket = realSocket;
if (this.#bufferedWrites.length === 0) logger$1.verbose("passthrough with empty writes buffer (state: %d)", this.readyState);
/**
* Flush any writes during the pending phase to the passthrough socket.
* @note These are written directly on the passthrough socket to prevent
* them from being forwarded as "data" events on the server (already emitted).
*/
for (let i = 0; i < this.#bufferedWrites.length; i++) {
const pendingWrite = this.#bufferedWrites[i];
if (i === 0 && typeof flushPendingData === "function") {
const data = pendingWrite[1];
const encoding = pendingWrite[2];
flushPendingData(data, encoding, (nextData) => {
pendingWrite[1] = nextData;
});
}
const [, data, encoding, callback] = pendingWrite;
writePendingData(realSocket, data, encoding, callback);
}
this.#bufferedWrites = [];
this.socket._pendingData = null;
this.socket._pendingEncoding = "";
this.socket.address = realSocket.address.bind(realSocket);
this.socket.removeListener("drain", this.#onMockSocketDrain);
this.socket.on("drain", this.#onMockSocketDrain);
realSocket.removeListener("connect", this.#onRealSocketConnect).removeListener("connectionAttemptFailed", this.#onRealSocketConnectionAttemptFailed).removeListener("connectionAttemptTimeout", this.#onRealSocketConnectionAttemptTimeout).removeListener("data", this.#onRealSocketData).removeListener("error", this.#onRealSocketError).removeListener("end", this.#onRealSocketEnd).removeListener("close", this.#onRealSocketClose);
realSocket.once("connect", this.#onRealSocketConnect).on("connectionAttemptFailed", this.#onRealSocketConnectionAttemptFailed).on("connectionAttemptTimeout", this.#onRealSocketConnectionAttemptTimeout).on("data", this.#onRealSocketData).on("error", this.#onRealSocketError).on("end", this.#onRealSocketEnd).on("close", this.#onRealSocketClose);
/**
* @note Forward the client's half-close, unless the real handle
* is already swapped in — the client's own shutdown then reaches
* the real connection natively (see "#forwardClientFinish").
*/
if (!this.#realHandleSwapped) if (this.socket.writableFinished) this.#forwardClientFinish();
else {
this.socket.removeListener("finish", this.#forwardClientFinish);
this.socket.once("finish", this.#forwardClientFinish);
}
return realSocket;
}
/**
* Create the passthrough connection to the target this controller
* was retargeted to (see `reset()`). The original `createConnection`
* dials the originally requested target and cannot serve retargeted
* exchanges.
*/
#createRetargetedConnection(connectionOptions) {
const realSocket = new net.Socket();
/**
* @note Mark the socket as patched before connecting: the patched
* "Socket.prototype.connect" exempts such sockets, establishing
* the connection for real.
*/
realSocket[kPatched] = true;
return realSocket.connect(connectionOptions);
}
};
var TlsSocketController = class extends TcpSocketController {
/**
* @note The TLS connection options must be provided explicitly.
* They cannot be captured from "socket.connect()" like for plain
* TCP sockets because "tls.connect()" fixes them at the TLS socket
* construction, before its transport ever connects.
*/
#tlsConnectionOptions;
constructor(socket, createConnection, tlsConnectionOptions) {
super(socket, createConnection, tlsConnectionOptions);
this.socket = socket;
this.createConnection = createConnection;
this.#tlsConnectionOptions = tlsConnectionOptions;
socket.prependListener("secureConnect", () => {
/**
* @note Reflect the negotiated ALPN protocol from the handle that
* completed the handshake. The socket sets "alpnProtocol" only in
* its own "_finishInit", which never runs for passthrough
* connections (the handshake completes on the passthrough socket
* whose handle this socket inherits).
*/
socket.alpnProtocol = socket._handle.getALPNNegotiatedProtocol();
});
}
emulateConnect() {
super.emulateConnect();
for (const listener of this.socket.rawListeners("secureConnect")) listener.apply(this.socket);
}
claim() {
/**
* @note The client may destroy the socket before the connection
* is claimed. Defer to the parent class, which transitions the
* ready state and skips the mock connection of destroyed sockets.
*/
if (this.socket.destroyed) {
super.claim();
return;
}
/**
* @note Reflect that the mocked connection is not authorized.
* There is no real peer certificate to verify. The identity check
* itself is skipped for claimed connections (see the
* "isSessionReused" mock below).
*/
this.socket.prependOnceListener("secureConnect", () => {
Reflect.set(this.socket, "authorized", false);
Reflect.set(this.socket, "authorizationError", "MOCKED_CONNECTION_NOT_VERIFIED");
});
const handle = this.socket._handle;
handle.start = () => void 0;
/**
* Mock this to prevent the "Error: Worker exited unexpectedly" error.
* This will trigger when "secure" is emitted.
* @see https://github.com/nodejs/node/blob/bdc8131fa78089b81b74dbff467365afb6536e6a/lib/internal/tls/wrap.js#L1648
*/
handle.verifyError = () => void 0;
handle.getSession = () => {
return Buffer.from("mocked session");
};
/**
* @note Skip the server identity check for this mocked connection.
* Node.js runs "options.checkServerIdentity" in "onConnectSecure"
* against the peer certificate, and a mocked connection has no
* real peer certificate — the caller's (or the default) validation
* would fail and destroy the socket. The check is overridden on the
* socket's internal connect options since the emulated handshake
* still completes through the regular "onConnectSecure" path.
* @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1621
*/
const realTlsConnectOptions = getTlsConnectOptions(this.socket);
if (realTlsConnectOptions) realTlsConnectOptions.checkServerIdentity = () => {};
handle.getCipher = () => {
return {
name: "TLS_AES_256_GCM_SHA384",
standardName: "TLS_AES_256_GCM_SHA384",
version: "TLSv1.3"
};
};
/**
* Mock this to prevent a segfault on Node.js 26+. The native
* implementation reads the negotiated group ("SSL_get0_group_name")
* of a handshake that never happened. Node.js itself calls this
* in "onConnectSecure" to validate the "minDHSize" option.
* Reflect the ephemeral key exchange matching the mocked cipher.
* @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1636
*/
handle.getEphemeralKeyInfo = () => {
return {
type: "ECDH",
name: "X25519",
size: 253
};
};
const requestedAlpnProtocols = this.#tlsConnectionOptions?.ALPNProtocols;
if (Array.isArray(requestedAlpnProtocols) && requestedAlpnProtocols.length > 0) {
const [preferredProtocol] = requestedAlpnProtocols;
/**
* @note Reflect the client's preferred ALPN protocol as the
* negotiated one. The mocked server accepts whatever the
* client prefers.
*/
handle.getALPNNegotiatedProtocol = () => {
return typeof preferredProtocol === "string" ? preferredProtocol : false;
};
}
this.socket.once("connect", () => {
/**
* @note A TLS 1.3 handshake derives five secrets, each reported
* via a separate "keylog" event before the handshake completes.
* Reflect them with mocked key material matching the mocked
* cipher (SHA-384 secrets; the format is "LABEL <client random>
* <secret>", each value hex-encoded).
*/
const mockedClientRandom = "0".repeat(64);
const mockedSecret = "0".repeat(96);
for (const keylogLabel of [
"SERVER_HANDSHAKE_TRAFFIC_SECRET",
"EXPORTER_SECRET",
"SERVER_TRAFFIC_SECRET_0",
"CLIENT_HANDSHAKE_TRAFFIC_SECRET",
"CLIENT_TRAFFIC_SECRET_0"
]) this.socket.emit("keylog", Buffer.from(`${keylogLabel} ${mockedClientRandom} ${mockedSecret}\n`));
handle.onhandshakedone();
/**
* @note A TLS 1.3 server issues two session tickets by default,
* each emitting a separate "session" event on the client.
*/
handle.onnewsession(1, Buffer.from("mocked session"));
handle.onnewsession(2, Buffer.from("mocked session"));
});
super.claim();
}
passthrough(flushPendingData) {
const realSocket = super.passthrough(flushPendingData);
/**
* @note Remove the internal "connect" listener added by the TLS socket.
* Normally, that listener manages the SSL handshake. But since we're in passthrough,
* we delegate that to the real socket. Leaving the listener on the mock socket while
* inheriting the real socket's handle will result in the handshake performed twice, which is a no-op.
* @see https://github.com/nodejs/node/blob/abddfc921bf2af02a04a6a5d2bca8e2d91d80958/lib/internal/tls/wrap.js#L1105
*
* This prevents the following error:
* # node (vitest 4)[8686]: static void node::crypto::TLSWrap::Start(const FunctionCallbackInfo<Value> &) at ../src/crypto/crypto_tls.cc:589
# Assertion failed: !wrap->started_
*/
for (const connectListener of this.socket.listeners("connect")) if (connectListener === this.socket._start || "listener" in connectListener && connectListener.listener === this.socket._start) this.socket.removeListener("connect", connectListener);
realSocket.on("secure", () => {
this.socket.emit("secure");
}).on("session", (...args) => {
this.socket.emit("session", ...args);
}).on("keylog", (...args) => {
this.socket.emit("keylog", ...args);
}).on("OCSPResponse", (...args) => {
this.socket.emit("OCSPResponse", ...args);
});
return realSocket;
}
};
//#endregion
//#region src/interceptors/net/utils/normalize-tls-connect-args.ts
function normalizeTlsConnectArgs(args) {
/**
* @note Despite incorrect type definitions, "tls.connect()" has all the
* options of "net.connect()" and then those specific to TLS connections,
* like "session" or "socket".
* @see https://github.com/nodejs/node/blob/bdc8131fa78089b81b74dbff467365afb6536e6a/lib/internal/tls/wrap.js#L1615
*/
const netConnectArgs = normalizeNetConnectArgs(args);
const options = netConnectArgs[0];
const callback = netConnectArgs[1];
if (args[0] !== null && typeof args[0] === "object") Object.assign(options, args[0]);
else if (args[1] !== null && typeof args[1] === "object") Object.assign(options, args[1]);
else if (args[2] !== null && typeof args[2] === "object") Object.assign(options, args[2]);
return callback ? [options, callback] : [options];
}
//#endregion
//#region src/interceptors/net/index.ts
var SocketConnectionEvent = class extends TypedEvent {
constructor(data) {
super(...["connection", {}]);
this.socket = data.socket;
this.connectionOptions = data.connectionOptions;
this.controller = data.controller;
}
};
const logger = createLogger("socket");
/**
* A DNS lookup function for intercepted sockets. It always succeeds,
* resolving any hostname to the loopback address. This ensures the
* "lookup"/"connectionAttempt" socket events fire even for non-existent
* hosts, and no real DNS resolution is performed. Passthrough
* connections are created with the original options and use the real
* (or the caller's custom) lookup instead.
*/
const mockLookup = (hostname, dnsOptions, callback) => {
const family = dnsOptions.family === 6 ? 6 : 4;
const address = family === 6 ? "::1" : "127.0.0.1";
/**
* @note Call back asynchronously since DNS lookup is always
* asynchronous in Node.js. Calling back synchronously emits
* the "lookup"/"connectionAttempt" socket events before the
* consumer gets a chance to add listeners for them.
*/
process.nextTick(() => {
/**
* @note Honor the Node.js lookup contract: the callback receives
* an array of addresses only when the "all" option is set
* (e.g. during the family autoselection). Otherwise, it receives
* a single address and its family. Node.js rejects an array in
* the latter case with "ERR_INVALID_IP_ADDRESS".
*/
if (dnsOptions.all) {
callback(null, [{
address,
family
}]);
return;
}
callback(null, address, family);
});
};
/**
* Interceptor for `net.Socket` connections.
*/
var SocketInterceptor = class extends Interceptor {
static {
this.symbol = Symbol.for("socket-interceptor");
}
predicate() {
return true;
}
setup() {
const interceptor = this;
/**
* @note A synchronous re-entrancy latch for creating passthrough
* TLS connections. The original "tls.connect()" constructs a TLS
* socket and calls "socket.connect()" on it synchronously, and that
* call must reach Node.js as-is instead of being intercepted again.
*/
let isCreatingPassthroughConnection = false;
this.subscriptions.push(
/**
* @note Intercept connections at the "net.Socket.prototype.connect"
* level instead of patching the "net.connect()" module function.
* ESM consumers snapshot the module bindings at import time
* ("import * as net from 'node:net'"), so reassigning "net.connect"
* is invisible to them. Every client connection ends up calling
* "Socket.prototype.connect" (including the one made by the original
* "net.connect()"), and prototype mutations are visible regardless
* of how the module was imported.
*/
patchesRegistry.applyPatch(net.Socket.prototype, "connect", (realSocketConnect) => {
return function connect(...args) {
const socket = this;
/**
* @note Skip the sockets this interceptor already controls
* (the mock connect call below, re-connects of an intercepted
* socket, passthrough sockets) and the sockets created while
* dialing a passthrough TLS connection. Their connects must
* reach Node.js as-is.
*/
if (socket[kPatched] || isCreatingPassthroughConnection) return realSocketConnect.apply(socket, args);
logger.verbose("socket.connect() %o", args);
/**
* @note The original "net.connect()"/"tls.connect()" normalize
* the arguments themselves and call this method with the
* normalized array instead of the individual arguments.
*/
const connectArgs = Array.isArray(args[0]) ? args[0] : args;
const [transportConnectionOptions, connectionCallback] = normalizeNetConnectArgs(connectArgs);
logger.verbose("connection options %o", {
transportConnectionOptions,
connectionCallback
});
let controller;
let connectionOptions;
if (socket instanceof tls.TLSSocket) {
/**
* @note TLS sockets arrive here from the original
* "tls.connect()", which connects the transport of the TLS
* socket it constructs ("connectionCallback" is its internal
* "_start" handshake trigger). The original TLS connection
* options are recovered from the socket instance since the
* construction has already happened.
*/
const realTlsConnectionOptions = getTlsConnectOptions(socket);
const [tlsConnectionOptions] = normalizeTlsConnectArgs([{
...transportConnectionOptions,
...realTlsConnectionOptions
}]);
connectionOptions = tlsConnectionOptions;
controller = new TlsSocketController(socket, () => {
/**
* @note Create the passthrough connection via the original
* "tls.connect()" with the original connection options
* (the real DNS lookup and the caller's certificate
* validation included). The latch exempts the transport
* connect of that connection from interception.
*/
isCreatingPassthroughConnection = true;
try {
return tls.connect(realTlsConnectionOptions ?? tlsConnectionOptions);
} finally {
isCreatingPassthroughConnection = false;
}
}, tlsConnectionOptions);
} else {
/**
* @note Create passthrough connections without the connection
* callback. The callback is already registered as a "connect"
* listener on the consumer's socket. Passing it to the
* passthrough connection would invoke it twice.
*/
const passthroughArgs = connectArgs.filter((arg) => {
return typeof arg !== "function";
});
/**
* @note Create the passthrough socket with the original
* options, the same way "net.connect()" does. Connect it via
* the unpatched method so the passthrough connection is not
* intercepted again.
*/
const socketOptions = connectArgs[0] !== null && typeof connectArgs[0] === "object" && !("href" in connectArgs[0]) ? connectArgs[0] : {};
connectionOptions = transportConnectionOptions;
controller = new TcpSocketController(socket, () => {
const passthroughSocket = new net.Socket(socketOptions);
Reflect.apply(realSocketConnect, passthroughSocket, passthroughArgs);
return passthroughSocket;
});
}
process.nextTick(() => {
if (socket.destroyed) return;
/**
* @note Expect a verdict on this connection from every
* "connection" listener before emitting the event. With no
* listeners to claim the connection (or once every listener
* declines it), the controller passes it through as-is.
*/
controller.awaitVerdicts(interceptor.listenerCount("connection"));
interceptor.emitter.emit(new SocketConnectionEvent({
socket: controller.serverSocket,
controller,
connectionOptions
}));
logger.verbose("emitted \"connection\" event!");
});
logger.verbose("connecting the socket...");
/**
* @note The requested local address/port are stripped from the
* actual "socket.connect()" call by the controller to prevent
* binding the intercepted socket (see the "connect" proxy).
*/
const mockConnectionOptions = { ...transportConnectionOptions };
mockConnectionOptions.lookup = mockLookup;
try {
/**
* @note The normalized options are looser than the declared
* "SocketConnectOpts" (e.g. the port may be a string when a
* URL is passed). Node.js validates them at runtime.
* This call goes through the controller's "connect" proxy
* and lands back in this patch, where the "kPatched" check
* above delegates it to the unpatched method.
*/
return socket.connect(mockConnectionOptions, connectionCallback ?? void 0);
} catch (error) {
/**
* @note "socket.connect()" can throw synchronously on invalid
* input (e.g. a bad port). Destroy the socket so the pending
* interception tick does not act on it, then let the error
* propagate to the consumer like in Node.js.
*/
socket.destroy();
throw error;
}
};
}),
this.#stopReusingUnpatchedSockets()
);
/**
* @note "net.connect()"/"net.createConnection()" need no patching of
* their own: both construct a "net.Socket" and call the patched
* "Socket.prototype.connect" on it.
*/
}
/**
* Prevent the `net.Socket` instances created before this interceptor
* was applied from getting reused by an `Agent`. Purging them from the
* keep-alive pool forces the agent to establish new (intercepted)
* connections instead.
*/
#stopReusingUnpatchedSockets() {
/**
* @note "Agent.prototype.addRequest" is undocumented but stable:
* its signature changed once (the keep-alive Agent rewrite in
* Node.js 0.12), and the ecosystem (agent-base, agentkeepalive,
* APM wrappers) has relied on it ever since. "https.Agent"
* inherits it, so a single patch covers both.
*/
if (typeof http.Agent.prototype.addRequest !== "function") return () => {};
return patchesRegistry.applyPatch(http.Agent.prototype, "addRequest", (realAddRequest) => {
return function(...args) {
/**
* @note Destroy the free sockets created before the interceptor
* was applied. Destroying flips their "destroyed" state
* synchronously, so the original "addRequest" below discards
* them and dials a new (intercepted) connection instead.
* The "freeSockets" pool only contains idle sockets by
* definition, so destroying them aborts nothing in-flight.
*/
for (const sockets of Object.values(this.freeSockets)) {
if (sockets == null) continue;
for (const socket of sockets) if (!socket[kPatched]) socket.destroy();
}
return realAddRequest?.apply(this, args);
};
});
}
};
//#endregion
export { getDeepPropertyDescriptor as a, unwrapPendingData as i, SocketController as n, patchesRegistry as o, kRawSocket as r, SocketInterceptor as t };
//# sourceMappingURL=net-9sRKnjIG.js.map

Sorry, the diff of this file is too big to display

import { a as createLogger, i as Interceptor, o as formatRequest, r as toBuffer } from "./buffer-utils-BvPY1Tc-.js";
import { a as isResponseError, c as isObject, d as createRequestId, f as RequestController, l as getRawFetchHeaders, n as FetchResponse, o as isResponseLike, p as InterceptorError, r as createServerErrorResponse, s as kErrorResponse, t as FetchRequest, u as recordRawFetchHeaders } from "./fetch-utils-Dw1PsmtX.js";
import { i as unwrapPendingData, n as SocketController, r as kRawSocket, t as SocketInterceptor } from "./net-9sRKnjIG.js";
import { TypedEvent } from "rettime";
import { invariant } from "outvariant";
import { IncomingMessage, METHODS, STATUS_CODES, ServerResponse } from "node:http";
import { AsyncLocalStorage } from "node:async_hooks";
import net from "node:net";
import tls from "node:tls";
import { Readable } from "node:stream";
import fs from "node:fs";
import { until } from "@open-draft/until";
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
//#region src/request-context.ts
const requestContext = new AsyncLocalStorage();
function runInRequestContext(callback, logger) {
/**
* @note Never shadow an existing request context. Nested calls
* (e.g. a patched entry point re-entered synchronously, or a request
* made within the fetch/XMLHttpRequest interceptor context) must run
* within the parent context so the sockets they create capture it.
*/
if (requestContext.getStore()) return callback();
/**
* @note The initiator is the callback's return value (e.g. the
* "ClientRequest" instance), so it cannot be known before running
* the callback. The context is mutated in place once the callback
* returns; readers hold the context object by reference and sample
* "initiator" only after the request has been written.
*/
const context = {
initiator: void 0,
logger
};
return requestContext.run(context, () => {
const initiator = callback();
context.initiator = initiator;
return initiator;
});
}
//#endregion
//#region src/interceptors/http/forward-events.ts
const logger = createLogger("http-request");
function forwardHttpEvents(options) {
const controller = new AbortController();
const { source, emitter, predicate, responsePredicate } = options;
source.on("request", async (event) => {
if (predicate(event.initiator)) {
logger.verbose("forwarding \"request\" event %o", { requestId: event.requestId });
await emitter.emitAsPromise(event);
}
}, { signal: controller.signal });
const responseListener = async (event) => {
if (predicate(event.initiator) && (responsePredicate == null || responsePredicate(event))) {
logger.verbose("forwarding \"response\" event %o", {
requestId: event.requestId,
responseType: event.responseType
});
await emitter.emitAsPromise(event);
}
};
const unhandledExceptionListener = async (event) => {
if (predicate(event.initiator)) {
logger.verbose("forwarding \"unhandledException\" event %o", { requestId: event.requestId });
await emitter.emitAsPromise(event);
}
};
const addResponseListener = () => {
if (!source.listeners("response").includes(responseListener)) source.on("response", responseListener, { signal: controller.signal });
};
const addUnhandledExceptionListener = () => {
if (!source.listeners("unhandledException").includes(unhandledExceptionListener)) source.on("unhandledException", unhandledExceptionListener, { signal: controller.signal });
};
if (emitter.listenerCount("response") > 0) addResponseListener();
if (emitter.listenerCount("unhandledException") > 0) addUnhandledExceptionListener();
emitter.hooks.on("newListener", (type) => {
if (type === "response") addResponseListener();
if (type === "unhandledException") addUnhandledExceptionListener();
}, {
signal: controller.signal,
persist: true
});
emitter.hooks.on("removeListener", (type) => {
if (type === "response" && emitter.listenerCount("response") === 0) source.removeListener("response", responseListener);
if (type === "unhandledException" && emitter.listenerCount("unhandledException") === 0) source.removeListener("unhandledException", unhandledExceptionListener);
}, {
signal: controller.signal,
persist: true
});
return () => {
controller.abort();
};
}
//#endregion
//#region src/events/http.ts
var HttpRequestEvent = class extends TypedEvent {
constructor(data) {
super(...["request", {}]);
this.request = data.request;
this.requestId = data.requestId;
this.initiator = data.initiator;
this.controller = data.controller;
}
};
var HttpResponseEvent = class extends TypedEvent {
constructor(data) {
super(...["response", {}]);
this.response = data.response;
this.responseType = data.responseType;
this.request = data.request;
this.requestId = data.requestId;
this.initiator = data.initiator;
}
};
var UnhandledHttpException = class extends TypedEvent {
constructor(data) {
super(...["unhandledException", {}]);
this.error = data.error;
this.request = data.request;
this.requestId = data.requestId;
this.initiator = data.initiator;
this.controller = data.controller;
}
};
//#endregion
//#region src/interceptors/net/utils/connection-options-to-url.ts
/**
* Creates a `URL` instance out of the `net.connect()` options.
* @note This implies that the passed connection is an HTTP connection.
*/
function connectionOptionsToUrl(options, socket) {
const isIPv6 = net.isIPv6(options.host || "");
const protocol = socket instanceof tls.TLSSocket ? "https:" : getProtocolByConnectionOptions(options);
const host = options.host || "localhost";
const url = new URL(`${protocol}//${isIPv6 ? `[${host}]` : host}`);
if (options.path) url.pathname = options.path;
if (options.port) url.port = options.port.toString();
if (options.auth) {
const [username, password] = options.auth.split(":");
/**
* Authentication options are provided as plain values.
* Encode them to form a valid URL.
* @see https://github.com/nodejs/node/blob/f3adc11e37b8bfaaa026ea85c1cf22e3a0e29ae9/lib/internal/url.js#L1452
*/
url.username = encodeURIComponent(username);
url.password = encodeURIComponent(password);
}
return url;
}
function getProtocolByConnectionOptions(options) {
if (options.protocol) return options.protocol;
if (options.port === 443) return "https:";
return "http:";
}
//#endregion
//#region src/interceptors/http/http-parser/index.ts
var import_constants = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports) => {
Object.defineProperty(exports, "__esModule", { value: true });
exports.SPECIAL_HEADERS = exports.MINOR = exports.MAJOR = exports.QDTEXT = exports.CONNECTION_TOKEN_CHARS = exports.RELAXED_HEADER_CHARS = exports.HEADER_CHARS = exports.HTAB_SP_VCHAR_OBS_TEXT = exports.SP = exports.HTAB = exports.TOKEN = exports.HEX = exports.URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.DIGIT = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.METHODS = exports.METHODS_HTTP = exports.METHODS_HTTP2 = exports.METHODS_HTTP1 = exports.METHODS_RTSP = exports.METHODS_RAOP = exports.METHODS_AIRPLAY = exports.METHODS_ICECAST = exports.METHODS_NON_STANDARD = exports.METHODS_CALDAV = exports.METHODS_UPNP = exports.METHODS_SUBVERSION = exports.METHODS_WEBDAV = exports.METHODS_BASIC_HTTP = exports.METHODS_HTTP1_HEAD = exports.HEADER_STATE = exports.FINISH = exports.STATUSES = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0;
exports.ERROR = {
OK: 0,
INTERNAL: 1,
STRICT: 2,
CR_EXPECTED: 25,
LF_EXPECTED: 3,
UNEXPECTED_CONTENT_LENGTH: 4,
UNEXPECTED_SPACE: 30,
CLOSED_CONNECTION: 5,
INVALID_METHOD: 6,
INVALID_URL: 7,
INVALID_CONSTANT: 8,
INVALID_VERSION: 9,
INVALID_HEADER_TOKEN: 10,
INVALID_CONTENT_LENGTH: 11,
INVALID_CHUNK_SIZE: 12,
INVALID_STATUS: 13,
INVALID_EOF_STATE: 14,
INVALID_TRANSFER_ENCODING: 15,
CB_MESSAGE_BEGIN: 16,
CB_HEADERS_COMPLETE: 17,
CB_MESSAGE_COMPLETE: 18,
CB_CHUNK_HEADER: 19,
CB_CHUNK_COMPLETE: 20,
PAUSED: 21,
PAUSED_UPGRADE: 22,
PAUSED_H2_UPGRADE: 23,
USER: 24,
CB_URL_COMPLETE: 26,
CB_STATUS_COMPLETE: 27,
CB_METHOD_COMPLETE: 32,
CB_VERSION_COMPLETE: 33,
CB_HEADER_FIELD_COMPLETE: 28,
CB_HEADER_VALUE_COMPLETE: 29,
CB_CHUNK_EXTENSION_NAME_COMPLETE: 34,
CB_CHUNK_EXTENSION_VALUE_COMPLETE: 35,
CB_RESET: 31,
CB_PROTOCOL_COMPLETE: 38
};
exports.TYPE = {
BOTH: 0,
REQUEST: 1,
RESPONSE: 2
};
exports.FLAGS = {
CONNECTION_KEEP_ALIVE: 1,
CONNECTION_CLOSE: 2,
CONNECTION_UPGRADE: 4,
CHUNKED: 8,
UPGRADE: 16,
CONTENT_LENGTH: 32,
SKIPBODY: 64,
TRAILING: 128,
TRANSFER_ENCODING: 512
};
exports.LENIENT_FLAGS = {
HEADERS: 1,
CHUNKED_LENGTH: 2,
KEEP_ALIVE: 4,
TRANSFER_ENCODING: 8,
VERSION: 16,
DATA_AFTER_CLOSE: 32,
OPTIONAL_LF_AFTER_CR: 64,
OPTIONAL_CRLF_AFTER_CHUNK: 128,
OPTIONAL_CR_BEFORE_LF: 256,
SPACES_AFTER_CHUNK_SIZE: 512,
HEADER_VALUE_RELAXED: 1024
};
exports.STATUSES = {
CONTINUE: 100,
SWITCHING_PROTOCOLS: 101,
PROCESSING: 102,
EARLY_HINTS: 103,
RESPONSE_IS_STALE: 110,
REVALIDATION_FAILED: 111,
DISCONNECTED_OPERATION: 112,
HEURISTIC_EXPIRATION: 113,
MISCELLANEOUS_WARNING: 199,
OK: 200,
CREATED: 201,
ACCEPTED: 202,
NON_AUTHORITATIVE_INFORMATION: 203,
NO_CONTENT: 204,
RESET_CONTENT: 205,
PARTIAL_CONTENT: 206,
MULTI_STATUS: 207,
ALREADY_REPORTED: 208,
TRANSFORMATION_APPLIED: 214,
IM_USED: 226,
MISCELLANEOUS_PERSISTENT_WARNING: 299,
MULTIPLE_CHOICES: 300,
MOVED_PERMANENTLY: 301,
FOUND: 302,
SEE_OTHER: 303,
NOT_MODIFIED: 304,
USE_PROXY: 305,
SWITCH_PROXY: 306,
TEMPORARY_REDIRECT: 307,
PERMANENT_REDIRECT: 308,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
PAYMENT_REQUIRED: 402,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
NOT_ACCEPTABLE: 406,
PROXY_AUTHENTICATION_REQUIRED: 407,
REQUEST_TIMEOUT: 408,
CONFLICT: 409,
GONE: 410,
LENGTH_REQUIRED: 411,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
URI_TOO_LONG: 414,
UNSUPPORTED_MEDIA_TYPE: 415,
RANGE_NOT_SATISFIABLE: 416,
EXPECTATION_FAILED: 417,
IM_A_TEAPOT: 418,
PAGE_EXPIRED: 419,
ENHANCE_YOUR_CALM: 420,
MISDIRECTED_REQUEST: 421,
UNPROCESSABLE_ENTITY: 422,
LOCKED: 423,
FAILED_DEPENDENCY: 424,
TOO_EARLY: 425,
UPGRADE_REQUIRED: 426,
PRECONDITION_REQUIRED: 428,
TOO_MANY_REQUESTS: 429,
REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL: 430,
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
LOGIN_TIMEOUT: 440,
NO_RESPONSE: 444,
RETRY_WITH: 449,
BLOCKED_BY_PARENTAL_CONTROL: 450,
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
CLIENT_CLOSED_LOAD_BALANCED_REQUEST: 460,
INVALID_X_FORWARDED_FOR: 463,
REQUEST_HEADER_TOO_LARGE: 494,
SSL_CERTIFICATE_ERROR: 495,
SSL_CERTIFICATE_REQUIRED: 496,
HTTP_REQUEST_SENT_TO_HTTPS_PORT: 497,
INVALID_TOKEN: 498,
CLIENT_CLOSED_REQUEST: 499,
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
HTTP_VERSION_NOT_SUPPORTED: 505,
VARIANT_ALSO_NEGOTIATES: 506,
INSUFFICIENT_STORAGE: 507,
LOOP_DETECTED: 508,
BANDWIDTH_LIMIT_EXCEEDED: 509,
NOT_EXTENDED: 510,
NETWORK_AUTHENTICATION_REQUIRED: 511,
WEB_SERVER_UNKNOWN_ERROR: 520,
WEB_SERVER_IS_DOWN: 521,
CONNECTION_TIMEOUT: 522,
ORIGIN_IS_UNREACHABLE: 523,
TIMEOUT_OCCURED: 524,
SSL_HANDSHAKE_FAILED: 525,
INVALID_SSL_CERTIFICATE: 526,
RAILGUN_ERROR: 527,
SITE_IS_OVERLOADED: 529,
SITE_IS_FROZEN: 530,
IDENTITY_PROVIDER_AUTHENTICATION_ERROR: 561,
NETWORK_READ_TIMEOUT: 598,
NETWORK_CONNECT_TIMEOUT: 599
};
exports.FINISH = {
SAFE: 0,
SAFE_WITH_CB: 1,
UNSAFE: 2
};
exports.HEADER_STATE = {
GENERAL: 0,
CONNECTION: 1,
CONTENT_LENGTH: 2,
TRANSFER_ENCODING: 3,
UPGRADE: 4,
CONNECTION_KEEP_ALIVE: 5,
CONNECTION_CLOSE: 6,
CONNECTION_UPGRADE: 7,
TRANSFER_ENCODING_CHUNKED: 8
};
exports.METHODS_HTTP1_HEAD = { HEAD: 2 };
/**
* HTTP methods as defined by RFC-9110 and other specifications.
* @see https://httpwg.org/specs/rfc9110.html#method.definitions
*/
exports.METHODS_BASIC_HTTP = {
DELETE: 0,
GET: 1,
...exports.METHODS_HTTP1_HEAD,
POST: 3,
PUT: 4,
CONNECT: 5,
OPTIONS: 6,
TRACE: 7,
/**
* @see https://www.rfc-editor.org/rfc/rfc5789.html
*/
PATCH: 28,
LINK: 31,
UNLINK: 32
};
exports.METHODS_WEBDAV = {
COPY: 8,
LOCK: 9,
MKCOL: 10,
MOVE: 11,
PROPFIND: 12,
PROPPATCH: 13,
SEARCH: 14,
UNLOCK: 15,
BIND: 16,
REBIND: 17,
UNBIND: 18,
ACL: 19
};
exports.METHODS_SUBVERSION = {
REPORT: 20,
MKACTIVITY: 21,
CHECKOUT: 22,
MERGE: 23
};
exports.METHODS_UPNP = {
"M-SEARCH": 24,
NOTIFY: 25,
SUBSCRIBE: 26,
UNSUBSCRIBE: 27
};
exports.METHODS_CALDAV = { MKCALENDAR: 30 };
exports.METHODS_NON_STANDARD = {
/**
* Not defined in any RFC but commonly used
*/
PURGE: 29,
QUERY: 46
};
exports.METHODS_ICECAST = { SOURCE: 33 };
exports.METHODS_AIRPLAY = {
GET: 1,
POST: 3
};
exports.METHODS_RAOP = { FLUSH: 45 };
exports.METHODS_RTSP = {
OPTIONS: exports.METHODS_BASIC_HTTP.OPTIONS,
DESCRIBE: 35,
ANNOUNCE: 36,
SETUP: 37,
PLAY: 38,
PAUSE: 39,
TEARDOWN: 40,
GET_PARAMETER: 41,
SET_PARAMETER: 42,
REDIRECT: 43,
RECORD: 44,
...exports.METHODS_AIRPLAY,
...exports.METHODS_RAOP
};
exports.METHODS_HTTP1 = {
...exports.METHODS_BASIC_HTTP,
...exports.METHODS_WEBDAV,
...exports.METHODS_SUBVERSION,
...exports.METHODS_UPNP,
...exports.METHODS_CALDAV,
...exports.METHODS_NON_STANDARD,
...exports.METHODS_ICECAST
};
exports.METHODS_HTTP2 = {
/**
* RFC-9113, section 11.6
* @see https://www.rfc-editor.org/rfc/rfc9113.html#preface
*/
PRI: 34 };
exports.METHODS_HTTP = {
...exports.METHODS_HTTP1,
...exports.METHODS_HTTP2
};
exports.METHODS = {
...exports.METHODS_HTTP1,
...exports.METHODS_HTTP2,
...exports.METHODS_RTSP
};
exports.ALPHA = [
"A",
"a",
"B",
"b",
"C",
"c",
"D",
"d",
"E",
"e",
"F",
"f",
"G",
"g",
"H",
"h",
"I",
"i",
"J",
"j",
"K",
"k",
"L",
"l",
"M",
"m",
"N",
"n",
"O",
"o",
"P",
"p",
"Q",
"q",
"R",
"r",
"S",
"s",
"T",
"t",
"U",
"u",
"V",
"v",
"W",
"w",
"X",
"x",
"Y",
"y",
"Z",
"z"
];
exports.NUM_MAP = {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9
};
exports.HEX_MAP = {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
A: 10,
B: 11,
C: 12,
D: 13,
E: 14,
F: 15,
a: 10,
b: 11,
c: 12,
d: 13,
e: 14,
f: 15
};
exports.DIGIT = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9"
];
exports.ALPHANUM = [...exports.ALPHA, ...exports.DIGIT];
exports.MARK = [
"-",
"_",
".",
"!",
"~",
"*",
"'",
"(",
")"
];
exports.USERINFO_CHARS = [
...exports.ALPHANUM,
...exports.MARK,
"%",
";",
":",
"&",
"=",
"+",
"$",
","
];
exports.URL_CHAR = [
"!",
"\"",
"$",
"%",
"&",
"'",
"(",
")",
"*",
"+",
",",
"-",
".",
"/",
":",
";",
"<",
"=",
">",
"@",
"[",
"\\",
"]",
"^",
"_",
"`",
"{",
"|",
"}",
"~",
...exports.ALPHANUM
];
exports.HEX = [
...exports.DIGIT,
"a",
"b",
"c",
"d",
"e",
"f",
"A",
"B",
"C",
"D",
"E",
"F"
];
exports.TOKEN = [
"!",
"#",
"$",
"%",
"&",
"'",
"*",
"+",
"-",
".",
"^",
"_",
"`",
"|",
"~",
...exports.ALPHANUM
];
exports.HTAB = [" "];
exports.SP = [" "];
const VCHAR = [
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126
];
const OBS_TEXT = [
128,
129,
130,
131,
132,
133,
134,
135,
136,
137,
138,
139,
140,
141,
142,
143,
144,
145,
146,
147,
148,
149,
150,
151,
152,
153,
154,
155,
156,
157,
158,
159,
160,
161,
162,
163,
164,
165,
166,
167,
168,
169,
170,
171,
172,
173,
174,
175,
176,
177,
178,
179,
180,
181,
182,
183,
184,
185,
186,
187,
188,
189,
190,
191,
192,
193,
194,
195,
196,
197,
198,
199,
200,
201,
202,
203,
204,
205,
206,
207,
208,
209,
210,
211,
212,
213,
214,
215,
216,
217,
218,
219,
220,
221,
222,
223,
224,
225,
226,
227,
228,
229,
230,
231,
232,
233,
234,
235,
236,
237,
238,
239,
240,
241,
242,
243,
244,
245,
246,
247,
248,
249,
250,
251,
252,
253,
254,
255
];
exports.HTAB_SP_VCHAR_OBS_TEXT = [
...exports.HTAB,
...exports.SP,
...VCHAR,
...OBS_TEXT
];
exports.HEADER_CHARS = exports.HTAB_SP_VCHAR_OBS_TEXT;
exports.RELAXED_HEADER_CHARS = [...[
1,
2,
3,
4,
5,
6,
7,
8,
11,
12,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
127
], ...exports.HEADER_CHARS];
exports.CONNECTION_TOKEN_CHARS = [
...exports.HTAB,
...exports.SP,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126,
...OBS_TEXT
];
exports.QDTEXT = [
...exports.HTAB,
...exports.SP,
33,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126,
...OBS_TEXT
];
exports.MAJOR = exports.NUM_MAP;
exports.MINOR = exports.MAJOR;
exports.SPECIAL_HEADERS = {
"connection": exports.HEADER_STATE.CONNECTION,
"content-length": exports.HEADER_STATE.CONTENT_LENGTH,
"proxy-connection": exports.HEADER_STATE.CONNECTION,
"transfer-encoding": exports.HEADER_STATE.TRANSFER_ENCODING,
"upgrade": exports.HEADER_STATE.UPGRADE
};
exports.default = {
ERROR: exports.ERROR,
TYPE: exports.TYPE,
FLAGS: exports.FLAGS,
LENIENT_FLAGS: exports.LENIENT_FLAGS,
STATUSES: exports.STATUSES,
FINISH: exports.FINISH,
HEADER_STATE: exports.HEADER_STATE,
ALPHA: exports.ALPHA,
NUM_MAP: exports.NUM_MAP,
HEX_MAP: exports.HEX_MAP,
DIGIT: exports.DIGIT,
ALPHANUM: exports.ALPHANUM,
MARK: exports.MARK,
USERINFO_CHARS: exports.USERINFO_CHARS,
URL_CHAR: exports.URL_CHAR,
HEX: exports.HEX,
TOKEN: exports.TOKEN,
HEADER_CHARS: exports.HEADER_CHARS,
RELAXED_HEADER_CHARS: exports.RELAXED_HEADER_CHARS,
CONNECTION_TOKEN_CHARS: exports.CONNECTION_TOKEN_CHARS,
QDTEXT: exports.QDTEXT,
HTAB_SP_VCHAR_OBS_TEXT: exports.HTAB_SP_VCHAR_OBS_TEXT,
MAJOR: exports.MAJOR,
MINOR: exports.MINOR,
SPECIAL_HEADERS: exports.SPECIAL_HEADERS,
METHODS: exports.METHODS,
METHODS_HTTP: exports.METHODS_HTTP,
METHODS_HTTP1_HEAD: exports.METHODS_HTTP1_HEAD,
METHODS_HTTP1: exports.METHODS_HTTP1,
METHODS_HTTP2: exports.METHODS_HTTP2,
METHODS_ICECAST: exports.METHODS_ICECAST,
METHODS_RTSP: exports.METHODS_RTSP
};
})))(), 1);
const KIND_REQUEST = import_constants.TYPE.REQUEST;
import_constants.TYPE.RESPONSE;
const kPointer = Symbol("kPtr");
const kUrl = Symbol("kUrl");
const kStatusMessage = Symbol("kStatusMessage");
const kHeadersFields = Symbol("kHeadersFields");
const kHeadersValues = Symbol("kHeadersValues");
const kLastHeaderCallback = Symbol("kLastHeaderCallback");
const kCallbacks = Symbol("kCallbacks");
const kType = Symbol("kType");
const HEADER_CB_NONE = 0;
const HEADER_CB_FIELD = 1;
const HEADER_CB_VALUE = 2;
const parsersMap = /* @__PURE__ */ new Map();
const methodNames = Object.fromEntries(Object.entries(import_constants.METHODS).map(([name, num]) => [num, name]));
function readStringFrom(pointer, length) {
return Buffer.from(llhttp_memory.buffer, pointer, length).toString("latin1");
}
/**
* @note Reference the base URL through a variable. Bundlers (e.g. Vite)
* statically rewrite the `new URL('...', import.meta.url)` pattern into
* an asset URL resolved against the served origin, which breaks reading
* the WASM binary from the file system in DOM-like test environments.
*/
const wasmBaseUrl = import.meta.url;
const llhttpModule = new WebAssembly.Module(fs.readFileSync(new URL("./llhttp/llhttp.wasm", wasmBaseUrl)));
const llhttpInstance = new WebAssembly.Instance(llhttpModule, { env: {
wasm_on_message_begin(parserPointer) {
const parser = parsersMap.get(parserPointer);
parser[kUrl] = "";
parser[kStatusMessage] = "";
parser[kHeadersFields] = [];
parser[kHeadersValues] = [];
parser[kLastHeaderCallback] = HEADER_CB_NONE;
return parser[kCallbacks].onMessageBegin?.() ?? 0;
},
wasm_on_url(parserPointer, at, length) {
parsersMap.get(parserPointer)[kUrl] = readStringFrom(at, length);
return 0;
},
wasm_on_status(parserPointer, at, length) {
parsersMap.get(parserPointer)[kStatusMessage] = readStringFrom(at, length);
return 0;
},
wasm_on_header_field(parserPointer, at, length) {
const parser = parsersMap.get(parserPointer);
const chunk = readStringFrom(at, length);
const fields = parser[kHeadersFields];
if (parser[kLastHeaderCallback] === HEADER_CB_FIELD) fields[fields.length - 1] += chunk;
else {
fields.push(chunk);
parser[kLastHeaderCallback] = HEADER_CB_FIELD;
}
return 0;
},
wasm_on_header_value(parserPointer, at, length) {
const parser = parsersMap.get(parserPointer);
const chunk = readStringFrom(at, length);
const values = parser[kHeadersValues];
if (parser[kLastHeaderCallback] === HEADER_CB_VALUE) values[values.length - 1] += chunk;
else {
values.push(chunk);
parser[kLastHeaderCallback] = HEADER_CB_VALUE;
}
return 0;
},
wasm_on_headers_complete(parserPointer, statusCode, rawUpgrade, rawShouldKeepAlive) {
const parser = parsersMap.get(parserPointer);
const versionMajor = llhttp_get_version_major(parserPointer);
const versionMinor = llhttp_get_version_minor(parserPointer);
const rawHeaders = [];
const upgrade = rawUpgrade === 1;
const shouldKeepAlive = rawShouldKeepAlive === 1;
for (let c = 0; c < parser[kHeadersFields].length; c++) rawHeaders.push(parser[kHeadersFields][c], parser[kHeadersValues][c]);
if (parser[kType] === KIND_REQUEST) {
const method = methodNames[llhttp_get_method(parserPointer)];
const url = parser[kUrl];
return parser[kCallbacks].onHeadersComplete?.({
versionMajor,
versionMinor,
rawHeaders,
method,
url,
upgrade,
shouldKeepAlive
}) ?? 0;
} else {
const statusCode = llhttp_get_status_code(parserPointer);
const statusMessage = parser[kStatusMessage];
return parser[kCallbacks].onHeadersComplete?.({
versionMajor,
versionMinor,
rawHeaders,
statusCode,
statusMessage,
upgrade,
shouldKeepAlive
}) ?? 0;
}
},
wasm_on_body(parserPointer, at, length) {
const parser = parsersMap.get(parserPointer);
const body = Buffer.from(new Uint8Array(llhttp_memory.buffer, at, length));
return parser[kCallbacks].onBody?.(body) ?? 0;
},
wasm_on_message_complete(parserPointer) {
return parsersMap.get(parserPointer)[kCallbacks].onMessageComplete?.() ?? 0;
}
} });
const llhttp_memory = llhttpInstance.exports.memory;
const llhttp_alloc = llhttpInstance.exports.llhttp_alloc;
const llhttp_malloc = llhttpInstance.exports.malloc;
const llhttp_execute = llhttpInstance.exports.llhttp_execute;
llhttpInstance.exports.llhttp_get_type;
llhttpInstance.exports.llhttp_get_upgrade;
llhttpInstance.exports.llhttp_should_keep_alive;
const llhttp_get_method = llhttpInstance.exports.llhttp_get_method;
const llhttp_get_status_code = llhttpInstance.exports.llhttp_get_status_code;
const llhttp_get_version_minor = llhttpInstance.exports.llhttp_get_http_minor;
const llhttp_get_version_major = llhttpInstance.exports.llhttp_get_http_major;
const llhttp_get_error_reason = llhttpInstance.exports.llhttp_get_error_reason;
const llhttp_get_error_pos = llhttpInstance.exports.llhttp_get_error_pos;
const llhttp_free = llhttpInstance.exports.free;
const initialize = llhttpInstance.exports._initialize;
initialize();
var HttpParser = class {
constructor(type, callbacks) {
this[kUrl] = "";
this[kStatusMessage] = "";
this[kHeadersFields] = [];
this[kHeadersValues] = [];
this[kLastHeaderCallback] = HEADER_CB_NONE;
this[kType] = type;
this[kCallbacks] = callbacks;
const parserPointer = llhttp_alloc(type);
if (parserPointer === 0) throw new Error("Failed to allocate llhttp parser");
this[kPointer] = parserPointer;
parsersMap.set(this[kPointer], this);
}
destroy() {
if (this[kPointer] === 0) return;
parsersMap.delete(this[kPointer]);
llhttp_free(this[kPointer]);
this[kPointer] = 0;
}
execute(data) {
const pointer = llhttp_malloc(data.byteLength);
if (pointer === 0) throw new Error("Failed to allocate llhttp input buffer");
let ret;
try {
new Uint8Array(llhttp_memory.buffer).set(data, pointer);
ret = llhttp_execute(this[kPointer], pointer, data.byteLength);
} catch (error) {
llhttp_free(pointer);
throw error;
}
if (ret === import_constants.ERROR.PAUSED_UPGRADE) {
const consumed = llhttp_get_error_pos(this[kPointer]) - pointer;
llhttp_free(pointer);
return data.subarray(consumed);
}
llhttp_free(pointer);
this.#checkError(ret);
return null;
}
#checkError(errorCode) {
if (errorCode === import_constants.ERROR.OK) return;
const errorPointer = llhttp_get_error_reason(this[kPointer]);
const length = new Uint8Array(llhttp_memory.buffer).indexOf(0, errorPointer) - errorPointer;
throw new Error(readStringFrom(errorPointer, length));
}
};
//#endregion
//#region src/interceptors/http/http-parser.ts
var HttpRequestParser = class extends HttpParser {
#requestBodyStream;
constructor(options) {
super(1, {
onHeadersComplete: ({ rawHeaders, method, url: path }) => {
/**
* @note When the socket is reused, "connectionOptions" will point
* to the "net.connect()" call options that established the connection,
* which may differ from the description of the current request (e.g. method).
* Rely on the HTTPParser supplying us with the correct "rawMethod" number.
*/
const finalMethod = (method || options.connectionOptions.method || "GET").toUpperCase();
const url = new URL(path || "", options.connectionOptions.url);
const headers = FetchResponse.parseRawHeaders([...rawHeaders]);
if (url.username || url.password) {
if (!headers.has("authorization")) {
const credentials = Buffer.from(`${url.username}:${url.password}`).toString("base64");
headers.set("authorization", `Basic ${credentials}`);
}
url.username = "";
url.password = "";
}
this.#requestBodyStream = new Readable({
/**
* @note Provide the `read()` method so a `Readable` could be
* used as the actual request body (the stream calls "read()").
*/
read: () => {} });
/**
* @note Expose an abort controller for the parsed request so the
* consumer can abort it (e.g. when the client destroys the
* connection before the request is handled).
*/
const abortController = new AbortController();
const request = new FetchRequest(url, {
method: finalMethod,
headers,
credentials: "same-origin",
body: Readable.toWeb(this.#requestBodyStream),
signal: abortController.signal
});
options.onRequest(request, abortController);
},
onBody: (chunk) => {
invariant(this.#requestBodyStream, "Failed to write to a request stream: stream does not exist. This is likely an issue with the library. Please report it on GitHub.");
this.#requestBodyStream.push(chunk);
},
onMessageComplete: () => {
this.#requestBodyStream?.push(null);
}
});
}
free() {
this.destroy();
this.#requestBodyStream?.destroy();
this.#requestBodyStream = void 0;
}
};
var HttpResponseParser = class extends HttpParser {
#responseBodyStream;
constructor(options) {
super(2, {
onHeadersComplete: ({ rawHeaders, statusCode: status, statusMessage: statusText }) => {
const headers = FetchResponse.parseRawHeaders([...rawHeaders]);
const response = new FetchResponse(FetchResponse.isResponseWithBody(status) ? Readable.toWeb(this.#responseBodyStream = new Readable({ read() {} })) : null, {
status,
statusText,
headers
});
options.onResponse(response);
},
onBody: (chunk) => {
invariant(this.#responseBodyStream, "Failed to read from a response stream: stream does not exist. This is likely an issue with the library. Please report it on GitHub.");
this.#responseBodyStream.push(chunk);
},
onMessageComplete: () => {
this.#responseBodyStream?.push(null);
}
});
}
free() {
this.destroy();
this.#responseBodyStream = null;
}
};
//#endregion
//#region src/utils/is-node-like-error.ts
function isNodeLikeError(error) {
if (error == null) return false;
if (!(error instanceof Error)) return false;
return "code" in error && "errno" in error;
}
//#endregion
//#region src/utils/handle-request.ts
async function handleRequest(options) {
if (options.logger?.isEnabled("default")) formatRequest(options.request).then((message) => {
options.logger?.info("[%s] %s", options.requestId, message);
});
const handleResponse = async (response) => {
if (response instanceof Error) {
await options.controller.errorWith(response);
return true;
}
if (isResponseError(response)) {
await options.controller.respondWith(response);
return true;
}
/**
* Handle normal responses or response-like objects.
* @note This must come before the arbitrary object check
* since Response instances are, in fact, objects.
*/
if (isResponseLike(response)) {
await options.controller.respondWith(response);
return true;
}
if (isObject(response)) {
await options.controller.errorWith(response);
return true;
}
return false;
};
const handleResponseError = async (error) => {
if (error instanceof InterceptorError) throw resultError;
if (isNodeLikeError(error)) {
await options.controller.errorWith(error);
return true;
}
if (error instanceof Response) return await handleResponse(error);
return false;
};
const requestAbortPromise = Promise.withResolvers();
let requestAbortReason;
let isRequestAborted = false;
const onAbort = () => {
isRequestAborted = true;
requestAbortReason = options.request.signal?.reason;
requestAbortPromise.reject(requestAbortReason);
};
/**
* @note `signal` is not always defined in React Native.
*/
if (options.request.signal) {
if (options.request.signal.aborted) {
await options.controller.errorWith(options.request.signal.reason);
return;
}
options.request.signal.addEventListener("abort", onAbort, { once: true });
}
const [resultError] = await until(async () => {
const requestEvent = new HttpRequestEvent({
initiator: options.initiator,
requestId: options.requestId,
request: options.request,
controller: options.controller
});
const requestListenersPromise = options.emitter.emitAsPromise(requestEvent);
await Promise.race([
requestAbortPromise.promise,
requestListenersPromise,
options.controller.handled
]);
/**
* @note If the "request" listener has replaced the request instance,
* propagate that mutation back to the underlying insterceptor.
* This happens with XMLHttpRequest that replaces request instances
* to correctly reflect the "withCredentials" option on the Fetch API request.
*/
if (requestEvent.request !== options.request) options.request = requestEvent.request;
});
options.request.signal?.removeEventListener("abort", onAbort);
if (isRequestAborted) {
await options.controller.errorWith(requestAbortReason);
return;
}
if (resultError) {
if (await handleResponseError(resultError)) return;
if (options.emitter.listenerCount("unhandledException") > 0) {
const unhandledExceptionController = new RequestController(options.request, {
/**
* @note Intentionally empty passthrough handle.
* This controller is created within another controller and we only need
* to know if `unhandledException` listeners handled the request.
*/
passthrough() {},
async respondWith(response) {
await handleResponse(response);
},
async errorWith(reason) {
/**
* @note Handle the result of the unhandled controller
* in the same way as the original request controller.
* The exception here is that thrown errors within the
* "unhandledException" event do NOT result in another
* emit of the same event. They are forwarded as-is.
*/
await options.controller.errorWith(reason);
}
});
await options.emitter.emitAsPromise(new UnhandledHttpException({
initiator: options.initiator,
error: resultError,
request: options.request,
requestId: options.requestId,
controller: unhandledExceptionController
}));
if (unhandledExceptionController.readyState !== RequestController.PENDING) return;
}
await options.controller.respondWith(createServerErrorResponse(resultError));
return;
}
if (options.controller.readyState === RequestController.PENDING) return await options.controller.passthrough();
return options.controller.handled;
}
//#endregion
//#region src/interceptors/http/source.ts
const httpLogger = createLogger("http-request");
/**
* Interceptor for HTTP requests in Node.js.
* Routes socket connections through an HTTP parser.
*/
var NodeHttpRequestSource = class extends Interceptor {
static {
this.symbol = Symbol.for("node-http-request-source");
}
predicate() {
return true;
}
setup() {
const socketInterceptor = Interceptor.singleton(SocketInterceptor);
socketInterceptor.apply(this);
this.subscriptions.push(() => {
socketInterceptor.dispose(this);
});
/**
* @note Record the raw values provided to Headers set/append
* in order to support "IncomingMessage.prototype.rawHeaders".
* This is meant for the headers in mocked responses.
*/
this.subscriptions.push(recordRawFetchHeaders());
const controller = new AbortController();
this.subscriptions.push(() => controller.abort());
socketInterceptor.on("connection", ({ connectionOptions, socket, controller: socketController }) => {
let isHttpConnection;
let requestParser;
let tunnelUrl;
let abortPendingRequest;
/**
* @note Capture the request context of the connection itself.
* The socket is created synchronously within the request async
* context (e.g. inside the patched `http.request()`), but the
* first data may reach the socket from a foreign context (e.g.
* a form-data stream piped into the request), where sampling
* the request context yields nothing.
*/
const connectionRequestContext = requestContext.getStore();
/**
* @note The client destroys the socket synchronously (e.g. Undici
* on request abort) but the socket teardown events ("error",
* "close") are emitted asynchronously, after the consumer has
* already observed the rejected request promise. Hook into the
* destroy itself so the pending request is aborted before any
* of its listeners can resume.
*/
const rawSocket = socketController[kRawSocket];
const realSocketDestroy = rawSocket._destroy.bind(rawSocket);
rawSocket._destroy = (error, callback) => {
abortPendingRequest?.();
realSocketDestroy(error, callback);
};
/**
* @note Only inspect the first sent packet to determine the protocol.
* A single socket cannot be used for different protocols.
*/
socket.on("data", (chunk) => {
if (isHttpConnection === false) return;
/**
* @note A mocked "CONNECT" request has established a tunnel.
* The data that follows belongs to a new exchange addressed to
* the tunnel target. The parser stopped at the tunnel boundary
* (HTTP upgrade semantics), so tear it down and detect the
* tunneled protocol anew, like on a fresh connection.
*/
if (tunnelUrl && requestParser) {
requestParser.free();
requestParser = void 0;
isHttpConnection = void 0;
/**
* @note Retarget the connection to the tunnel authority.
* The exchanges that follow belong to the tunnel target,
* so an unclaimed exchange (HTTP or not) must pass through
* to that target — not to the proxy, which never actually
* established this tunnel — like a real established tunnel
* relays its traffic.
*/
socketController.reset({
host: tunnelUrl.hostname,
port: Number(tunnelUrl.port) || 80,
path: null
});
}
if (requestParser) {
requestParser.execute(toBuffer(chunk));
return;
}
const httpMessage = chunk.toString();
const httpMethod = httpMessage.split(" ")[0] || "";
if (!METHODS.includes(httpMethod.toUpperCase())) {
isHttpConnection = false;
socketController.decline();
return;
}
isHttpConnection = true;
const baseUrl = tunnelUrl ?? connectionOptionsToUrl(connectionOptions, socket);
httpLogger.verbose("handling http message %o", {
httpMessage,
httpMethod,
baseUrl
});
const requestContextValue = requestContext.getStore() ?? connectionRequestContext;
const initiator = requestContextValue?.initiator || socket;
requestParser = new HttpRequestParser({
connectionOptions: {
method: httpMethod,
url: baseUrl
},
onRequest: async (parsedRequest, requestAbortController) => {
const request = requestContextValue?.transformRequest?.(parsedRequest) ?? parsedRequest;
/**
* @note A subsequent request arriving on a kept-alive socket
* that has already been handled (passed through or mocked).
* Clients like Undici reuse sockets without emitting the
* "free" event, so reset the controller here, at the HTTP
* message boundary, to handle the new request from the
* pending state again.
*/
if (socketController["readyState"] !== SocketController.PENDING) socketController.reset();
const requestId = createRequestId();
const requestLogger = requestContextValue?.logger ?? httpLogger;
httpLogger.verbose("received a parsed HTTP request %o", {
method: request.method,
url: request.url
});
const requestController = new RequestController(request, {
respondWith: async (rawResponse) => {
httpLogger.verbose("respondWith() %o", {
status: rawResponse.status,
statusText: rawResponse.statusText,
hasBody: rawResponse.body != null
});
/**
* @note The client may destroy the socket (e.g. on request
* abort) moments before a response arrives. A destroyed
* socket cannot be claimed and has no one reading it.
*/
if (socket.destroyed) return;
socketController.claim();
const response = FetchResponse.from(rawResponse, { url: request.url });
/**
* @note A successful mocked response to a "CONNECT"
* request establishes a tunnel to the requested authority
* (e.g. "127.0.0.1:80"). The exchange that follows on this
* socket is addressed to that authority, not to the proxy.
*/
if (request.method === "CONNECT" && response.ok) tunnelUrl = new URL(`http://${request.url}`);
/**
* @note Clone the response before "respondWith" because it will
* consume its body. This way, we can have a readable response copy
* for the "response" event below.
*/
const responseClone = isResponseError(response) ? null : response.clone();
const respond = () => {
return this.respondWith({
socket: socketController[kRawSocket],
request: context.request,
response
});
};
if (responseClone) await this.emitter.emitAsPromise(new HttpResponseEvent({
initiator,
requestId,
request: context.request,
response: responseClone,
responseType: "mock"
}));
if (socket.connecting) socket.once("connect", respond);
else
/**
* @note Reused sockets stay connected between requests and will not
* emit "connect" anymore. If that's the case, respond immediately.
*/
await respond();
},
errorWith: (reason) => {
if (reason instanceof Error) socket.destroy(reason);
},
passthrough: () => {
const realSocket = socketController.passthrough(this.#modifyHttpHeaders(context.request));
if (this.emitter.listenerCount("response") > 0) {
httpLogger.verbose("found \"response\" listener, corking socket reads");
/**
* Suspend the delivery of the original response to the client
* until the "response" event listeners settle. This guarantees
* that the request promise (e.g. `await fetch()`) does not
* resolve before the listeners are done. The real socket keeps
* emitting data for the response parser meanwhile.
*/
socketController.corkReads();
const responseParser = new HttpResponseParser({ onResponse: async (response) => {
httpLogger.verbose("HTTP response parser parsed: %d %s", response.status, response.statusText);
if (isResponseError(response)) {
httpLogger.verbose("response is an error response, uncorking socket reads...");
socketController.uncorkReads();
return;
}
FetchResponse.setUrl(request.url, response);
try {
httpLogger.verbose("emitting \"response\" event");
await this.emitter.emitAsPromise(new HttpResponseEvent({
initiator,
requestId,
request: context.request,
response,
responseType: "original"
}));
} finally {
httpLogger.verbose("uncorking socket reads");
socketController.uncorkReads();
/**
* @note Informational responses other than
* "101 Switching Protocols" are followed by a final
* response on the same connection. Keep gating that
* final response on the "response" event listeners.
*/
if (response.status < 200 && response.status !== 101) socketController.corkReads();
}
} });
realSocket.on("data", (chunk) => responseParser.execute(chunk)).on("close", () => responseParser.free());
}
}
}, {
logger: requestLogger,
requestId
});
invariant(socketController["readyState"] === SocketController.PENDING, "CANNOT HANDLE ALREADY HANDLED REQUEST", request.method, request.url, socketController["readyState"]);
/**
* @note Create a request resolution context.
* This is so modifications to the "request" in upstream interceptors
* are correctly picked up by the underlying HTTP interceptor.
*/
const context = {
initiator,
requestId,
request,
controller: requestController,
emitter: this.emitter,
logger: requestLogger
};
/**
* @note The client destroying the socket while the request
* is still pending means the request was aborted (e.g. via
* `AbortController`). Abort the parsed request so its
* handling settles and late interactions with the request
* controller become controlled errors.
*/
abortPendingRequest = () => {
if (requestController.readyState === RequestController.PENDING) requestAbortController.abort();
};
try {
await handleRequest(context);
} finally {
abortPendingRequest = void 0;
}
}
});
requestParser.execute(toBuffer(chunk));
});
socket.on("close", () => requestParser?.free());
}, { signal: controller.signal });
}
async respondWith(args) {
const { socket, request, response } = args;
if (socket.destroyed) return;
if (isResponseError(response)) {
/**
* @note Reference the error response on the socket error so the
* client-side interceptors (e.g. fetch) can surface it to the
* consumer as the reason behind the failed request. Keep the
* reference non-enumerable so the error remains observably
* identical for the clients that expose it as-is.
*/
socket.destroy(Object.defineProperty(/* @__PURE__ */ new TypeError("Network error"), kErrorResponse, {
value: response,
enumerable: false
}));
return;
}
invariant(!socket.connecting, "Failed to mock a response for \"%s %s\": socket has not connected", request.method, request.url);
/**
* Use native server response handling in Node.js.
* @see https://github.com/nodejs/node/blob/13eb80f3b718452213e0fc449702aefbbfe4110f/lib/_http_server.js#L202
*/
const incomingMessage = new IncomingMessage(socket);
/**
* @note Describe the request method so the response body is
* handled appropriately (e.g. "HEAD" responses must not write
* a body). The HTTP version is deliberately left unset: with it,
* `ServerResponse` frames bodies of unknown length as chunked,
* polluting the mocked response headers with "Transfer-Encoding"
* the mock never specified.
*/
incomingMessage.method = request.method;
const serverResponse = new ServerResponse(incomingMessage);
const responseSocket = new net.Socket();
responseSocket._writeGeneric = (writev, data, encoding, callback) => {
unwrapPendingData(data, (chunk, encoding) => {
socket.push(toBuffer(chunk), encoding);
});
callback?.();
};
responseSocket._destroy = (error, callback) => {
/**
* Only destroy the socket on stream errors.
* On a clean end, the socket is already signaled via `socket.push(null)`
* in the main response flow. Destroying it here prematurely would prevent
* the client from processing the response (e.g. calling `response.destroy()`).
* @see https://github.com/mswjs/interceptors/issues/738
*/
if (error) socket.destroy();
callback(null);
};
responseSocket.on("drain", () => serverResponse.emit("drain"));
serverResponse.assignSocket(responseSocket);
serverResponse.removeHeader("connection");
serverResponse.removeHeader("date");
const rawResponseHeaders = getRawFetchHeaders(response.headers);
serverResponse.writeHead(response.status, response.statusText || STATUS_CODES[response.status], rawResponseHeaders);
/**
* @note Override the socket's `_destroy` before writing the response body.
* The underlying TCP handle (from `socket.connect()`) makes `_destroy` async
* (`_handle.close()` callback), which delays the 'error' event. Since the real
* TCP connection is irrelevant for mocked responses, take the synchronous path
* so that user-initiated `response.destroy(error)` emits the error promptly.
* This must happen before `serverResponse.end()` because the HTTP parser may
* fire the 'response' event synchronously during `socket.push()`.
*/
socket._destroy = function(error, callback) {
if (error)
/**
* Emit the error event as a microtask instead of relying on the default
* `process.nextTick(emitErrorNT)` from `callback(error)`. This is necessary
* because `respondWith` runs inside a microtask (from `await reader.read()`).
* A resolved promise continuation (from toWebResponse) is queued as
* another microtask during the same phase. Since microtasks are drained before
* nextTick, the test's `await` would resolve before the error event fires.
* Using `queueMicrotask` ensures the error event is emitted within the current
* microtask phase, before other queued microtasks.
*/
queueMicrotask(() => this.emit("error", error));
callback(null);
/**
* @note `net.Socket` is constructed with `emitClose: false`, so Node's
* stream destroy machinery does not emit `'close'` automatically; the
* stock `net.Socket._destroy` only emits it via `_handle.close()`.
* Since this override replaces `_destroy`, emit `'close'` here so the
* mocked socket completes its lifecycle (otherwise consumers waiting
* on `'close'`, like `http.ClientRequest`, hang).
*/
process.nextTick(() => this.emit("close", error != null));
};
if (response.body) {
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
serverResponse.end();
break;
}
if (!serverResponse.write(value)) await new Promise((resolve) => {
serverResponse.once("drain", resolve);
});
}
} catch {
/**
* @note Delay the socket destruction to allow the event loop
* to flush already-pushed response data (headers + body chunks)
* through the HTTP parser. Without this, the socket is destroyed
* on the same tick as `socket.push(data)` and the client never
* reads the response.
*/
await new Promise((resolve) => process.nextTick(resolve));
socket.destroy();
return;
}
} else serverResponse.end();
/**
* @note Self-delimiting responses (chunked, explicit "Content-Length",
* or bodiless by definition) must NOT signal the end-of-stream.
* The client parser completes them from their framing alone, and
* ending the socket would kill the kept-alive connection that
* agents pool and reuse for subsequent requests.
*/
const isSelfDelimitingResponse = request.method === "HEAD" || response.headers.has("content-length") || response.headers.has("transfer-encoding") || !FetchResponse.isResponseWithBody(response.status);
if (request.method === "CONNECT" && !response.ok || request.method !== "CONNECT" && !isSelfDelimitingResponse) {
/**
* @note Defer the end-of-stream signal so the HTTP parser has a chance
* to process already-pushed response data and fire the 'response' event
* before the socket is ended. Without this, the parser marks the response
* as "complete" before the client can interact with it (e.g. `response.destroy()`).
*/
await new Promise((resolve) => process.nextTick(resolve));
socket.push(null);
}
}
#modifyHttpHeaders(request) {
const transformRequestMessage = (httpMessage, encoding) => {
/**
* @note Socket can write a buffer (e.g. uploaded file) even before
* it writes the HTTP message. Bypass those cases.
*/
if (encoding === "buffer") return httpMessage;
const parts = httpMessage.toString(encoding).split("\r\n");
const headersEndIndex = parts.findIndex((field) => field === "");
const httpMessageHeaderPairs = parts.slice(1, headersEndIndex);
const httpMessageRawHeaders = httpMessageHeaderPairs.map((line) => {
const separatorIndex = line.indexOf(": ");
return [line.slice(0, separatorIndex), line.slice(separatorIndex + 2)];
});
const requestRawHeaders = getRawFetchHeaders(request.headers);
if (httpMessageRawHeaders.length === requestRawHeaders.length && httpMessageRawHeaders.every((tuple, index) => {
const requestTuple = requestRawHeaders[index];
return tuple[0] === requestTuple[0] && tuple[1] === requestTuple[1];
})) return httpMessage;
const httpMessageHeaders = FetchResponse.parseRawHeaders(httpMessageHeaderPairs.flatMap((header) => header.split(": ")));
const visitedHeaders = /* @__PURE__ */ new Set();
for (const [headerName] of requestRawHeaders) {
const normalizedHeaderName = headerName.toLowerCase();
if (visitedHeaders.has(normalizedHeaderName)) continue;
visitedHeaders.add(normalizedHeaderName);
/**
* @note Forbidden Fetch headers (e.g. Host, Origin, Connection)
* are stripped from `request.headers` but remain in the raw
* headers list. Skip them so the original values from the
* outgoing HTTP message are preserved.
*/
const headerValue = request.headers.get(headerName);
if (headerValue === null) continue;
httpMessageHeaders.set(headerName, headerValue);
}
visitedHeaders.clear();
const httpMessageHeadersString = Array.from(httpMessageHeaders).map(([name, value]) => `${name}: ${value}`).join("\r\n");
parts.splice(1, headersEndIndex - 1, httpMessageHeadersString);
return parts.join("\r\n");
};
return (pendingData, encoding, callback) => {
if (Array.isArray(pendingData)) pendingData[0].chunk = transformRequestMessage(pendingData[0].chunk, pendingData[0].encoding);
else pendingData = transformRequestMessage(pendingData, encoding);
callback(pendingData);
};
}
};
//#endregion
export { requestContext as a, forwardHttpEvents as i, handleRequest as n, runInRequestContext as o, HttpResponseEvent as r, NodeHttpRequestSource as t };
//# sourceMappingURL=source-BUVce4Q1.js.map

Sorry, the diff of this file is too big to display