import * as net from "./net.ts";import assert from "./internal/assert.js";import { once } from "./internal/util.js";import { _checkIsHttpToken as checkIsHttpToken, freeParser, HTTPParser, isLenient, parsers, prepareError,} from "./_http_common.ts";import { OutgoingMessage } from "./_http_outgoing.ts";import Agent from "./_http_agent.js";import { Buffer } from "./buffer.ts";import { defaultTriggerAsyncIdScope } from "./internal/async_hooks.ts";import { urlToHttpOptions } from "./internal/url.ts";import { kNeedDrain, kOutHeaders } from "./internal/http.ts";import { connResetException, ERR_HTTP_HEADERS_SENT, ERR_INVALID_ARG_TYPE, ERR_INVALID_HTTP_TOKEN, ERR_INVALID_PROTOCOL, ERR_UNESCAPED_CHARACTERS,} from "./_errors.ts";import { validateInteger } from "./internal/validators.js";import { getTimerDuration } from "./internal/timers.ts";import { DTRACE_HTTP_CLIENT_REQUEST, DTRACE_HTTP_CLIENT_RESPONSE,} from "./internal/dtrace.ts";
import { addAbortSignal, finished } from "./stream.ts";import { debuglog } from "./internal/util/debuglog.ts";
let debug = debuglog("http", (fn) => { debug = fn;});
const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;const kError = Symbol("kError");
const kLenientAll = HTTPParser.kLenientAll | 0;const kLenientNone = HTTPParser.kLenientNone | 0;
function validateHost(host, name) { if (host !== null && host !== undefined && typeof host !== "string") { throw new ERR_INVALID_ARG_TYPE(`options.${name}`, [ "string", "undefined", "null", ], host); } return host;}
class HTTPClientAsyncResource { constructor(type, req) { this.type = type; this.req = req; }}
export function ClientRequest(input, options, cb) { OutgoingMessage.call(this);
if (typeof input === "string") { const urlStr = input; input = urlToHttpOptions(new URL(urlStr)); } else if (input instanceof URL) { input = urlToHttpOptions(input); } else { cb = options; options = input; input = null; }
if (typeof options === "function") { cb = options; options = input || {}; } else { options = Object.assign(input || {}, options); }
let agent = options.agent; const defaultAgent = options._defaultAgent || Agent.globalAgent; if (agent === false) { agent = new defaultAgent.constructor(); } else if (agent === null || agent === undefined) { if (typeof options.createConnection !== "function") { agent = defaultAgent; } } else if (typeof agent.addRequest !== "function") { throw new ERR_INVALID_ARG_TYPE("options.agent", [ "Agent-like Object", "undefined", "false", ], agent); } this.agent = agent;
const protocol = options.protocol || defaultAgent.protocol; let expectedProtocol = defaultAgent.protocol; if (this.agent && this.agent.protocol) { expectedProtocol = this.agent.protocol; }
if (options.path) { const path = String(options.path); if (INVALID_PATH_REGEX.test(path)) { throw new ERR_UNESCAPED_CHARACTERS("Request path"); } }
if (protocol !== expectedProtocol) { throw new ERR_INVALID_PROTOCOL(protocol, expectedProtocol); }
const defaultPort = options.defaultPort || (this.agent && this.agent.defaultPort);
const port = options.port = options.port || defaultPort || 80; const host = options.host = validateHost(options.hostname, "hostname") || validateHost(options.host, "host") || "localhost";
const setHost = (options.setHost === undefined || Boolean(options.setHost));
this.socketPath = options.socketPath;
if (options.timeout !== undefined) { this.timeout = getTimerDuration(options.timeout, "timeout"); }
const signal = options.signal; if (signal) { addAbortSignal(signal, this); } let method = options.method; const methodIsString = (typeof method === "string"); if (method !== null && method !== undefined && !methodIsString) { throw new ERR_INVALID_ARG_TYPE("options.method", "string", method); }
if (methodIsString && method) { if (!checkIsHttpToken(method)) { throw new ERR_INVALID_HTTP_TOKEN("Method", method); } method = this.method = method.toUpperCase(); } else { method = this.method = "GET"; }
const maxHeaderSize = options.maxHeaderSize; if (maxHeaderSize !== undefined) { validateInteger(maxHeaderSize, "maxHeaderSize", 0); } this.maxHeaderSize = maxHeaderSize;
const insecureHTTPParser = options.insecureHTTPParser; if ( insecureHTTPParser !== undefined && typeof insecureHTTPParser !== "boolean" ) { throw new ERR_INVALID_ARG_TYPE( "options.insecureHTTPParser", "boolean", insecureHTTPParser, ); } this.insecureHTTPParser = insecureHTTPParser;
this.path = options.path || "/"; if (cb) { this.once("response", cb); }
if ( method === "GET" || method === "HEAD" || method === "DELETE" || method === "OPTIONS" || method === "TRACE" || method === "CONNECT" ) { this.useChunkedEncodingByDefault = false; } else { this.useChunkedEncodingByDefault = true; }
this._ended = false; this.res = null; this.aborted = false; this.timeoutCb = null; this.upgradeOrConnect = false; this.parser = null; this.maxHeadersCount = null; this.reusedSocket = false; this.host = host; this.protocol = protocol;
if (this.agent) { if (!this.agent.keepAlive && !Number.isFinite(this.agent.maxSockets)) { this._last = true; this.shouldKeepAlive = false; } else { this._last = false; this.shouldKeepAlive = true; } }
const headersArray = Array.isArray(options.headers); if (!headersArray) { if (options.headers) { const keys = Object.keys(options.headers); for (let i = 0; i < keys.length; i++) { const key = keys[i]; this.setHeader(key, options.headers[key]); } }
if (host && !this.getHeader("host") && setHost) { let hostHeader = host;
const posColon = hostHeader.indexOf(":"); if ( posColon !== -1 && hostHeader.includes(":", posColon + 1) && hostHeader.charCodeAt(0) !== 91 ) { hostHeader = `[${hostHeader}]`; }
if (port && +port !== defaultPort) { hostHeader += ":" + port; } this.setHeader("Host", hostHeader); }
if (options.auth && !this.getHeader("Authorization")) { this.setHeader( "Authorization", "Basic " + Buffer.from(options.auth).toString("base64"), ); }
if (this.getHeader("expect")) { if (this._header) { throw new ERR_HTTP_HEADERS_SENT("render"); }
this._storeHeader( this.method + " " + this.path + " HTTP/1.1\r\n", this[kOutHeaders], ); } } else { this._storeHeader( this.method + " " + this.path + " HTTP/1.1\r\n", options.headers, ); }
let optsWithoutSignal = options; if (optsWithoutSignal.signal) { optsWithoutSignal = Object.assign({}, options); delete optsWithoutSignal.signal; }
if (this.agent) { this.agent.addRequest(this, optsWithoutSignal); } else { this._last = true; this.shouldKeepAlive = false; if (typeof optsWithoutSignal.createConnection === "function") { const oncreate = once((err, socket) => { if (err) { process.nextTick(() => this.emit("error", err)); } else { this.onSocket(socket); } });
try { const newSocket = optsWithoutSignal.createConnection( optsWithoutSignal, oncreate, ); if (newSocket) { oncreate(null, newSocket); } } catch (err) { oncreate(err); } } else { debug("CLIENT use net.createConnection", optsWithoutSignal); this.onSocket(net.createConnection(optsWithoutSignal)); } }}Object.setPrototypeOf(ClientRequest.prototype, OutgoingMessage.prototype);Object.setPrototypeOf(ClientRequest, OutgoingMessage);
ClientRequest.prototype._finish = function _finish() { DTRACE_HTTP_CLIENT_REQUEST(this, this.socket); OutgoingMessage.prototype._finish.call(this);};
ClientRequest.prototype._implicitHeader = function _implicitHeader() { if (this._header) { throw new ERR_HTTP_HEADERS_SENT("render"); } this._storeHeader( this.method + " " + this.path + " HTTP/1.1\r\n", this[kOutHeaders], );};
ClientRequest.prototype.abort = function abort() { if (this.aborted) { return; } this.aborted = true; process.nextTick(emitAbortNT, this); this.destroy();};
ClientRequest.prototype.destroy = function destroy(err) { if (this.destroyed) { return this; } this.destroyed = true;
if (this.res) { this.res._dump?.(); }
this[kError] = err; this.socket?.destroy(err);
return this;};
function emitAbortNT(req) { req.emit("abort");}
function ondrain() { const msg = this._httpMessage; if (msg && !msg.finished && msg[kNeedDrain]) { msg[kNeedDrain] = false; msg.emit("drain"); }}
function socketCloseListener() { const socket = this; const req = socket._httpMessage; debug("HTTP socket close");
const parser = socket.parser; const res = req.res;
req.destroyed = true; if (res) { if (!res.complete) { res.destroy(connResetException("aborted")); } req._closed = true; req.emit("close"); if (!res.aborted && res.readable) { res.push(null); } } else { if (!req.socket._hadError) { req.socket._hadError = true; req.emit("error", connResetException("socket hang up")); } req._closed = true; req.emit("close"); }
if (req.outputData) { req.outputData.length = 0; }
if (parser) { parser.finish(); freeParser(parser, req, socket); }}
function socketErrorListener(err) { const socket = this; const req = socket._httpMessage; debug("SOCKET ERROR:", err.message, err.stack);
if (req) { req.socket._hadError = true; req.emit("error", err); }
const parser = socket.parser; if (parser) { parser.finish(); freeParser(parser, req, socket); }
socket.removeListener("data", socketOnData); socket.removeListener("end", socketOnEnd); socket.destroy();}
function socketOnEnd() { const socket = this; const req = this._httpMessage; const parser = this.parser;
if (!req.res && !req.socket._hadError) { req.socket._hadError = true; req.emit("error", connResetException("socket hang up")); } if (parser) { parser.finish(); freeParser(parser, req, socket); } socket.destroy();}
function socketOnData(d) { const socket = this; const req = this._httpMessage; const parser = this.parser;
assert(parser && parser.socket === socket);
const ret = parser.execute(d); if (ret instanceof Error) { prepareError(ret, parser, d); debug("parse error", ret); freeParser(parser, req, socket); socket.removeListener("data", socketOnData); socket.removeListener("end", socketOnEnd); socket.destroy(); req.socket._hadError = true; req.emit("error", ret); } else if (parser.incoming && parser.incoming.upgrade) { const bytesParsed = ret; const res = parser.incoming; req.res = res;
socket.removeListener("data", socketOnData); socket.removeListener("end", socketOnEnd); socket.removeListener("drain", ondrain);
if (req.timeoutCb) socket.removeListener("timeout", req.timeoutCb); socket.removeListener("timeout", responseOnTimeout);
parser.finish(); freeParser(parser, req, socket);
const bodyHead = d.slice(bytesParsed, d.length);
const eventName = req.method === "CONNECT" ? "connect" : "upgrade"; if (req.listenerCount(eventName) > 0) { req.upgradeOrConnect = true;
socket.emit("agentRemove"); socket.removeListener("close", socketCloseListener); socket.removeListener("error", socketErrorListener);
socket._httpMessage = null; socket.readableFlowing = null;
req.emit(eventName, res, socket, bodyHead); req.destroyed = true; req._closed = true; req.emit("close"); } else { socket.destroy(); } } else if ( parser.incoming && parser.incoming.complete && !statusIsInformational(parser.incoming.statusCode) ) { socket.removeListener("data", socketOnData); socket.removeListener("end", socketOnEnd); socket.removeListener("drain", ondrain); freeParser(parser, req, socket); }}
function statusIsInformational(status) { return (status < 200 && status >= 100 && status !== 101);}
function parserOnIncomingClient(res, shouldKeepAlive) { const socket = this.socket; const req = socket._httpMessage;
debug("AGENT incoming response!");
if (req.res) { socket.destroy(); return 0; } req.res = res;
if (res.upgrade) { return 2; }
const method = req.method; if (method === "CONNECT") { res.upgrade = true; return 2; }
if (statusIsInformational(res.statusCode)) { req.res = null; if (res.statusCode === 100) { req.emit("continue"); } req.emit("information", { statusCode: res.statusCode, statusMessage: res.statusMessage, httpVersion: res.httpVersion, httpVersionMajor: res.httpVersionMajor, httpVersionMinor: res.httpVersionMinor, headers: res.headers, rawHeaders: res.rawHeaders, });
return 1; }
if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) { req.shouldKeepAlive = false; }
DTRACE_HTTP_CLIENT_RESPONSE(socket, req); req.res = res; res.req = req;
res.on("end", responseOnEnd); req.on("prefinish", requestOnPrefinish); socket.on("timeout", responseOnTimeout);
if (req.aborted || !req.emit("response", res)) { res._dump(); }
if (method === "HEAD") { return 1; }
if (res.statusCode === 304) { res.complete = true; return 1; }
return 0; }
function responseKeepAlive(req) { const socket = req.socket;
debug("AGENT socket keep-alive"); if (req.timeoutCb) { socket.setTimeout(0, req.timeoutCb); req.timeoutCb = null; } socket.removeListener("close", socketCloseListener); socket.removeListener("error", socketErrorListener); socket.removeListener("data", socketOnData); socket.removeListener("end", socketOnEnd);
const asyncId = socket._handle ? socket._handle.getAsyncId() : undefined; defaultTriggerAsyncIdScope(asyncId, process.nextTick, emitFreeNT, req);
req.destroyed = true; if (req.res) { req.res.socket = null; }}
function responseOnEnd() { const req = this.req; const socket = req.socket;
if (socket) { if (req.timeoutCb) socket.removeListener("timeout", emitRequestTimeout); socket.removeListener("timeout", responseOnTimeout); }
req._ended = true;
if (!req.shouldKeepAlive) { if (socket.writable) { debug("AGENT socket.destroySoon()"); if (typeof socket.destroySoon === "function") { socket.destroySoon(); } else { socket.end(); } } assert(!socket.writable); } else if (req.finished && !this.aborted) { responseKeepAlive(req); }}
function responseOnTimeout() { const req = this._httpMessage; if (!req) return; const res = req.res; if (!res) return; res.emit("timeout");}
function requestOnPrefinish() { const req = this;
if (req.shouldKeepAlive && req._ended) { responseKeepAlive(req); }}
function emitFreeNT(req) { req._closed = true; req.emit("close"); if (req.socket) { req.socket.emit("free"); }}
function tickOnSocket(req, socket) { const parser = parsers.alloc(); req.socket = socket; const lenient = req.insecureHTTPParser === undefined ? isLenient() : req.insecureHTTPParser; parser.initialize( HTTPParser.RESPONSE, new HTTPClientAsyncResource("HTTPINCOMINGMESSAGE", req), req.maxHeaderSize || 0, lenient ? kLenientAll : kLenientNone, 0, ); parser.socket = socket; parser.outgoing = req; req.parser = parser;
socket.parser = parser; socket._httpMessage = req;
if (typeof req.maxHeadersCount === "number") { parser.maxHeaderPairs = req.maxHeadersCount << 1; }
parser.onIncoming = parserOnIncomingClient; socket.on("error", socketErrorListener); socket.on("data", socketOnData); socket.on("end", socketOnEnd); socket.on("close", socketCloseListener); socket.on("drain", ondrain);
if ( req.timeout !== undefined || (req.agent && req.agent.options && req.agent.options.timeout) ) { listenSocketTimeout(req); } req.emit("socket", socket);}
function emitRequestTimeout() { const req = this._httpMessage; if (req) { req.emit("timeout"); }}
function listenSocketTimeout(req) { if (req.timeoutCb) { return; } req.timeoutCb = emitRequestTimeout; if (req.socket) { req.socket.once("timeout", emitRequestTimeout); } else { req.on("socket", (socket) => { socket.once("timeout", emitRequestTimeout); }); }}
ClientRequest.prototype.onSocket = function onSocket(socket, err) { process.nextTick(onSocketNT, this, socket, err);};
function onSocketNT(req, socket, err) { if (req.destroyed || err) { req.destroyed = true;
function _destroy(req, err) { if (!req.aborted && !err) { err = connResetException("socket hang up"); } if (err) { req.emit("error", err); } req._closed = true; req.emit("close"); }
if (socket) { if (!err && req.agent && !socket.destroyed) { socket.emit("free"); } else { finished(socket.destroy(err || req[kError]), (er) => { if (er?.code === "ERR_STREAM_PREMATURE_CLOSE") { er = null; } _destroy(req, er || err); }); return; } }
_destroy(req, err || req[kError]); } else { tickOnSocket(req, socket); req._flush(); }}
ClientRequest.prototype._deferToConnect = _deferToConnect;function _deferToConnect(method, arguments_) {
const callSocketMethod = () => { if (method) { Reflect.apply(this.socket[method], this.socket, arguments_); } };
const onSocket = () => { if (this.socket.writable) { callSocketMethod(); } else { this.socket.once("connect", callSocketMethod); } };
if (!this.socket) { this.once("socket", onSocket); } else { onSocket(); }}
ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) { if (this._ended) { return this; }
listenSocketTimeout(this); msecs = getTimerDuration(msecs, "msecs"); if (callback) this.once("timeout", callback);
if (this.socket) { setSocketTimeout(this.socket, msecs); } else { this.once("socket", (sock) => setSocketTimeout(sock, msecs)); }
return this;};
function setSocketTimeout(sock, msecs) { if (sock.connecting) { sock.once("connect", function () { sock.setTimeout(msecs); }); } else { sock.setTimeout(msecs); }}
ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) { this._deferToConnect("setNoDelay", [noDelay]);};
ClientRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive( enable, initialDelay,) { this._deferToConnect("setKeepAlive", [enable, initialDelay]);};
ClientRequest.prototype.clearTimeout = function clearTimeout(cb) { this.setTimeout(0, cb);};
export default { ClientRequest,};