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