/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/node/polyfills/_http_client.js
1 613 строк
46 KB
Bartek Iwańczuk
chore: bump node_compat test suite to Node.js 26.5.1 (#36391)
03 авг 2026, 12:52
Не верифицирован
03 авг 2026, 12:52
bca6182
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. // Ported from Node.js lib/_http_client.js // deno-lint-ignore-file no-this-alias no-inner-declarations import { core, internals, primordials } from "ext:core/mod.js"; import { op_node_http_check_proxy_net } from "ext:core/ops"; const { ArrayIsArray, ArrayPrototypeIndexOf, ArrayPrototypePush, ArrayPrototypeSplice, Boolean, DateNow, ErrorPrototype, FunctionPrototypeCall, NumberIsFinite, NumberParseInt, ObjectAssign, ObjectDefineProperty, ObjectKeys, ObjectPrototypeIsPrototypeOf, ObjectSetPrototypeOf, ReflectApply, SafeArrayIterator, SafeRegExp, String, StringPrototypeCharCodeAt, StringPrototypeIncludes, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, StringPrototypeToLowerCase, StringPrototypeToUpperCase, StringPrototypeTrim, Symbol, } = primordials; import net from "node:net"; const { ok: assert } = core.loadExtScript("ext:deno_node/assert.ts"); const { kEmptyObject, once } = core.loadExtScript( "ext:deno_node/internal/util.mjs", ); import { _checkInvalidHeaderChar as checkInvalidHeaderChar, _checkIsHttpToken as checkIsHttpToken, freeParser, HTTPParser, isLenient, kSkipPendingData, parsers, prepareError, } from "node:_http_common"; import { kUniqueHeaders, OutgoingMessage, parseUniqueHeadersOption, } from "node:_http_outgoing"; import httpAgent from "node:_http_agent"; import httpProxy from "node:_http_proxy"; const { Buffer } = core.loadExtScript("ext:deno_node/internal/buffer.mjs"); const { urlToHttpOptions } = core.loadExtScript( "ext:deno_node/internal/url.ts", ); const { kOutHeaders } = core.loadExtScript("ext:deno_node/internal/http.ts"); const { connResetException, ERR_HTTP_HEADERS_SENT, ERR_INVALID_ARG_TYPE, ERR_INVALID_HTTP_TOKEN, ERR_INVALID_PROTOCOL, ERR_INVALID_URL, ERR_UNESCAPED_CHARACTERS, } = core.loadExtScript("ext:deno_node/internal/errors.ts"); const { validateBoolean, validateInteger, } = core.loadExtScript("ext:deno_node/internal/validators.mjs"); const { getTimerDuration } = core.loadExtScript( "ext:deno_node/internal/timers.mjs", ); import { addAbortSignal, finished } from "node:stream"; const { nextTick } = core.loadExtScript("ext:deno_node/_next_tick.ts"); const { defaultTriggerAsyncIdScope } = core.loadExtScript( "ext:deno_node/internal/async_hooks.ts", ); const { kNeedDrain } = core.loadExtScript("ext:deno_node/internal/http.ts"); const { channel } = core.loadExtScript("ext:deno_node/diagnostics_channel.js"); const { enqueueNodePerformanceEntry } = core.loadExtScript( "ext:deno_node/perf_hooks.js", ); const onClientRequestCreatedChannel = channel("http.client.request.created"); const onClientRequestStartChannel = channel("http.client.request.start"); const onClientRequestErrorChannel = channel("http.client.request.error"); const onClientResponseFinishChannel = channel("http.client.response.finish"); const { updateSpanFromError } = core.loadExtScript( "ext:deno_telemetry/util.ts", ); const { otelState, builtinTracer, ContextManager, SPAN_KEY, } = core.loadExtScript("ext:deno_telemetry/telemetry.ts"); const INVALID_PATH_REGEX = new SafeRegExp(/[^\u0021-\u00ff]/); const kError = Symbol("kError"); const kPath = Symbol("kPath"); const kOtelSpan = Symbol("kOtelSpan"); const kPerfStartTime = Symbol("kPerfStartTime"); const kRetryData = Symbol("kRetryData"); const kRetryDataSize = Symbol("kRetryDataSize"); const kRetryOptions = Symbol("kRetryOptions"); const kProxy = Symbol("kProxy"); const kProxyTargetHost = Symbol("kProxyTargetHost"); const kProxyTargetPort = Symbol("kProxyTargetPort"); const kInspectorRequestId = Symbol("kInspectorRequestId"); const kInspectorNetwork = Symbol("kInspectorNetwork"); const kInspectorUrl = Symbol("kInspectorUrl"); const kInspectorCompleted = Symbol("kInspectorCompleted"); // Bound replay buffering for stale keep-alive retry. Larger streaming uploads // keep constant-memory behavior and surface the socket error instead. const MAX_RETRY_DATA_SIZE = 1024 * 1024; // ============================================================================ // Inspector Network domain instrumentation (Chrome DevTools Protocol). // // When `node:inspector` has been loaded and `--inspect` is active, node:http // client requests emit `Network.requestWillBeSent` / `responseReceived` / // `dataReceived` / `loadingFinished` / `loadingFailed` events. The emitters // and a monotonic requestId generator are installed by // `ext/node/polyfills/inspector.js` onto `internals.__inspectorNetwork`, // so this layer is one `isEnabled()` check when the inspector is detached. // // Mirrors the implementation in ext/fetch/26_fetch.js. Differences: // - `type: "Other"` rather than `"Fetch"` (matches Chrome DevTools' contract // for non-fetch HTTP). // - Request headers are read from `kOutHeaders` rather than a flat list. // - Response body bytes are observed by wrapping `res.push`, not by teeing // a ReadableStream, since IncomingMessage is a Node Readable. The wrapper // stays out of the read pipeline so the response remains paused until the // consumer attaches a `data` listener (asserted by the upstream test). // ============================================================================ function getInspectorNetwork() { const ins = internals.__inspectorNetwork; if (ins && ins.isEnabled()) return ins; return null; } function safeEmit(fn, params) { try { fn(params); } catch { // Inspector emission is purely observational - never surface as an // http error to user code. } } function joinRequestHeadersForCdp(req) { const out = { __proto__: null }; const headers = req[kOutHeaders]; if (!headers) return out; const keys = ObjectKeys(headers); for (let i = 0; i < keys.length; i++) { const lower = keys[i]; // kOutHeaders keys are already lowercase const entry = headers[lower]; if (!entry) continue; const value = entry[1]; let separator; if (lower === "cookie") { separator = "; "; } else if (lower === "set-cookie") { separator = "\n"; } else { separator = ", "; } if (ArrayIsArray(value)) { let joined = ""; for (let j = 0; j < value.length; j++) { if (j > 0) joined += separator; joined += String(value[j]); } out[lower] = joined; } else { out[lower] = String(value); } } return out; } function joinResponseHeadersForCdp(rawHeaders) { const out = { __proto__: null }; if (!rawHeaders) return out; for (let i = 0; i < rawHeaders.length; i += 2) { const rawName = rawHeaders[i]; const value = String(rawHeaders[i + 1]); const lower = StringPrototypeToLowerCase(String(rawName)); let separator; if (lower === "cookie") { separator = "; "; } else if (lower === "set-cookie") { separator = "\n"; } else { separator = ", "; } if (out[lower] === undefined) { out[lower] = value; } else { out[lower] = out[lower] + separator + value; } } return out; } function parseContentTypeFromRawHeaders(rawHeaders) { let raw = null; if (rawHeaders) { for (let i = 0; i < rawHeaders.length; i += 2) { if ( StringPrototypeToLowerCase(String(rawHeaders[i])) === "content-type" ) { raw = String(rawHeaders[i + 1]); break; } } } if (raw === null) return { mimeType: "", charset: "" }; const semi = StringPrototypeIndexOf(raw, ";"); const mimeType = semi === -1 ? StringPrototypeTrim(raw) : StringPrototypeTrim(StringPrototypeSlice(raw, 0, semi)); let charset = ""; if (semi !== -1) { const rest = StringPrototypeSlice(raw, semi + 1); const parts = StringPrototypeSplit(rest, ";"); for (let i = 0; i < parts.length; i++) { const p = StringPrototypeTrim(parts[i]); if ( StringPrototypeStartsWith(StringPrototypeToLowerCase(p), "charset=") ) { charset = StringPrototypeTrim(StringPrototypeSlice(p, 8)); if ( charset.length >= 2 && charset[0] === '"' && charset[charset.length - 1] === '"' ) { charset = StringPrototypeSlice(charset, 1, charset.length - 1); } break; } } } return { mimeType, charset }; } function buildInspectorRequestUrl(protocol, host, port, path) { if ( path && (StringPrototypeStartsWith(path, "http://") || StringPrototypeStartsWith(path, "https://")) ) { return path; } let hostPart = host || "localhost"; if ( StringPrototypeIndexOf(hostPart, ":") !== -1 && StringPrototypeCharCodeAt(hostPart, 0) !== 91 /* '[' */ ) { hostPart = `[${hostPart}]`; } const proto = protocol || "http:"; const defaultPort = proto === "https:" ? 443 : 80; const portStr = port && +port !== defaultPort ? `:${port}` : ""; return `${proto}//${hostPart}${portStr}${path || "/"}`; } function inspectorEmitRequestWillBeSent(req, port) { const ins = getInspectorNetwork(); if (!ins) return; const requestId = ins.nextRequestId(); req[kInspectorRequestId] = requestId; req[kInspectorNetwork] = ins; const url = buildInspectorRequestUrl( req.protocol, req.host, port, req[kPath], ); req[kInspectorUrl] = url; const headers = joinRequestHeadersForCdp(req); const now = DateNow() / 1000; safeEmit(ins.requestWillBeSent, { requestId, timestamp: now, wallTime: now, type: "Other", request: { url, method: req.method, headers, hasPostData: false, }, }); } function inspectorEmitResponseReceived(req, res) { const ins = req[kInspectorNetwork]; const requestId = req[kInspectorRequestId]; if (!ins || !requestId) return; const rawHeaders = res.rawHeaders; const headers = joinResponseHeadersForCdp(rawHeaders); const { mimeType, charset } = parseContentTypeFromRawHeaders(rawHeaders); safeEmit(ins.responseReceived, { requestId, timestamp: DateNow() / 1000, type: "Other", response: { url: req[kInspectorUrl], status: res.statusCode, statusText: res.statusMessage || "", headers, mimeType, charset, }, }); // Wrap res.push to observe body chunks and the end-of-stream sentinel // (parserOnBody -> stream.push(buf); parserOnMessageComplete -> // stream.push(null)). Going through `data` events would put the stream // in flowing mode, breaking the upstream test that requires it to stay // paused until the consumer attaches a `data` listener. let totalLength = 0; const origPush = res.push; res.push = function inspectorPush(chunk, encoding) { if (chunk === null) { if (!req[kInspectorCompleted]) { req[kInspectorCompleted] = true; safeEmit(ins.loadingFinished, { requestId, timestamp: DateNow() / 1000, encodedDataLength: totalLength, }); } } else if (chunk) { let len; if (typeof chunk === "string") { len = chunk.length; // deno-lint-ignore deno-internal/prefer-primordials } else if (chunk.byteLength !== undefined) { // deno-lint-ignore deno-internal/prefer-primordials len = chunk.byteLength; } else { len = 0; } if (len > 0) { totalLength += len; safeEmit(ins.dataReceived, { requestId, timestamp: DateNow() / 1000, dataLength: len, encodedDataLength: len, data: chunk, }); } } return FunctionPrototypeCall(origPush, this, chunk, encoding); }; } function inspectorEmitLoadingFailed(req, error) { const ins = req[kInspectorNetwork]; const requestId = req[kInspectorRequestId]; if (!ins || !requestId || req[kInspectorCompleted]) return; req[kInspectorCompleted] = true; let errorText; if (error && typeof error === "object" && error.message !== undefined) { errorText = String(error.message); } else { errorText = String(error); } safeEmit(ins.loadingFailed, { requestId, timestamp: DateNow() / 1000, type: "Other", errorText, }); } const kLenientAll = HTTPParser.kLenientAll | 0; const kLenientNone = HTTPParser.kLenientNone | 0; class HTTPClientAsyncResource { constructor(type, req) { this.type = type; this.req = req; } } 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; } function emitErrorEvent(request, error) { if (onClientRequestErrorChannel.hasSubscribers) { onClientRequestErrorChannel.publish({ request, error, }); } // ---- Inspector: Network.loadingFailed ---------------------------------- // Fired before the user's `error` listener so DevTools sees the failure // even if the listener throws. inspectorEmitLoadingFailed(request, error); request.emit("error", error); } function isURL(input) { return ObjectPrototypeIsPrototypeOf(URL.prototype, input); } function ClientRequest(input, options, cb) { FunctionPrototypeCall(OutgoingMessage, this); if (typeof input === "string") { const urlStr = input; // Match Node: `new URL(...)` in ClientRequest surfaces as // ERR_INVALID_URL (node's internal URL constructor calls // bindingUrl.parse with raiseException=true). Deno's Web URL // throws a generic TypeError, so wrap it to attach the code. let parsed; try { parsed = new URL(urlStr); } catch { throw new ERR_INVALID_URL(urlStr); } input = urlToHttpOptions(parsed); } else if (isURL(input)) { input = urlToHttpOptions(input); } else { cb = options; options = input; input = null; } if (typeof options === "function") { cb = options; options = input || kEmptyObject; } else { options = ObjectAssign(input || {}, options); } let agent = options.agent; const defaultAgent = options._defaultAgent || httpAgent.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?.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?.defaultPort); const optsWithoutSignal = { __proto__: null, ...options }; // The `_proxy*` fields are internal transport details set only by the proxy // selection below. A caller must not be able to supply them directly: doing // so would route the request through an arbitrary proxy while bypassing the // target permission check that the proxy branch performs. Strip any that came // in via `options` so only the values computed here are honored. delete optsWithoutSignal._proxy; delete optsWithoutSignal._proxyTargetHost; delete optsWithoutSignal._proxyTargetPort; delete optsWithoutSignal._proxyProtocol; delete optsWithoutSignal._proxyUseProxyConnection; const port = optsWithoutSignal.port = options.port || defaultPort || 80; const host = optsWithoutSignal.host = validateHost(options.hostname, "hostname") || validateHost(options.host, "host") || "localhost"; // Proxy detection: if an env-derived proxy applies to this request, // either rewrite to absolute URL (http target) or set up a CONNECT tunnel // via a custom createConnection (https target). The agent's socket pool // is keyed by target host:port so users can look it up the same way as // a direct connection - the proxy is a transport detail tracked under // _proxy on the options. const proxyConfig = httpProxy.resolveAgentProxyConfig(this.agent); const proxyEntry = httpProxy.selectProxy( proxyConfig, protocol, host, port, ); if (proxyEntry) { // A proxied request connects only to the proxy, so the target host would // otherwise never be permission-checked. Enforce --allow-net for the // target here, matching fetch(), before routing through the proxy. // // A host or port carrying invalid header characters (e.g. CR/LF) is not a // reachable target: node:http rejects it with ERR_INVALID_CHAR while // building the request. Skip the permission check for those so the op does // not pre-empt that error with a parse/permission failure. Every other // target is permission-checked and a denial fails closed, like fetch(). if ( !checkInvalidHeaderChar(host) && !checkInvalidHeaderChar(String(port)) ) { // `port` may be a numeric string; coerce and clamp to the op's u16 range // so the argument conversion never throws. const targetPort = NumberParseInt(port, 10); op_node_http_check_proxy_net( host, NumberIsFinite(targetPort) && targetPort >= 0 && targetPort <= 65535 ? targetPort : 0, protocol === "https:" ? "node:https.request()" : "node:http.request()", ); } this[kProxy] = proxyEntry; this[kProxyTargetHost] = host; this[kProxyTargetPort] = port; optsWithoutSignal._proxy = proxyEntry; optsWithoutSignal._proxyTargetHost = host; optsWithoutSignal._proxyTargetPort = port; optsWithoutSignal._proxyProtocol = protocol; optsWithoutSignal._proxyUseProxyConnection = !(this.agent && this.agent.__proxyConfig !== undefined) || this.agent?.keepAlive === true; } const setHost = options.setHost !== undefined ? Boolean(options.setHost) : options.setDefaultHeaders !== false; this._removedConnection = options.setDefaultHeaders === false; this._removedContLen = options.setDefaultHeaders === false; this._removedTE = options.setDefaultHeaders === false; this.socketPath = options.socketPath; if (options.timeout !== undefined) { this.timeout = getTimerDuration(options.timeout, "timeout"); } const signal = options.signal; if (signal) { addAbortSignal(signal, this); delete optsWithoutSignal.signal; } let method = options.method; if (method != null) { if (typeof method !== "string") { throw new ERR_INVALID_ARG_TYPE("options.method", "string", method); } } if (method) { if (!checkIsHttpToken(method)) { throw new ERR_INVALID_HTTP_TOKEN("Method", method); } method = this.method = StringPrototypeToUpperCase(method); } 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) { validateBoolean(insecureHTTPParser, "options.insecureHTTPParser"); } this.insecureHTTPParser = insecureHTTPParser; if (options.joinDuplicateHeaders !== undefined) { validateBoolean( options.joinDuplicateHeaders, "options.joinDuplicateHeaders", ); } this.joinDuplicateHeaders = options.joinDuplicateHeaders; this[kPath] = options.path || "/"; // For HTTP via HTTP proxy, rewrite path to an absolute URL so the proxy // knows where to forward the request. if (this[kProxy] && protocol === "http:") { const t = this[kProxyTargetHost]; const formattedHost = t && StringPrototypeIndexOf(t, ":") !== -1 && StringPrototypeCharCodeAt(t, 0) !== 91 ? `[${t}]` : t; this[kPath] = `http://${formattedHost}:${this[kProxyTargetPort]}${ 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; this[kPerfStartTime] = performance.now(); if (this.agent) { if (!this.agent.keepAlive && !NumberIsFinite(this.agent.maxSockets)) { this._last = true; this.shouldKeepAlive = false; } else { this._last = false; this.shouldKeepAlive = true; } } const headersArray = ArrayIsArray(options.headers); if (!headersArray) { if (options.headers) { const keys = ObjectKeys(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 = StringPrototypeIndexOf(hostHeader, ":"); if ( posColon !== -1 && StringPrototypeIncludes(hostHeader, ":", posColon + 1) && StringPrototypeCharCodeAt(hostHeader, 0) !== 91 /* '[' */ ) { hostHeader = `[${hostHeader}]`; } if (port && +port !== defaultPort) { hostHeader += ":" + port; } // The request target of a CONNECT is the authority being tunnelled to, // so Host has to name that authority rather than the proxy we dialed. if (method === "CONNECT" && options.path) { hostHeader = String(options.path); } this.setHeader("Host", hostHeader); } if (options.auth && !this.getHeader("Authorization")) { this.setHeader( "Authorization", // deno-lint-ignore deno-internal/prefer-primordials "Basic " + Buffer.from(options.auth).toString("base64"), ); } if (this[kProxy] && protocol === "http:") { // Mirror what _storeHeader will pick for Connection: when shouldKeepAlive // is true, both Connection and Proxy-Connection are "keep-alive"; when // false, both are "close". Matches Node's wire format on the proxy hop. if (!this.getHeader("proxy-connection")) { this.setHeader( "Proxy-Connection", this.shouldKeepAlive ? "keep-alive" : "close", ); } if (this[kProxy].auth && !this.getHeader("proxy-authorization")) { this.setHeader("Proxy-Authorization", this[kProxy].auth); } } 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, ); } this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders); // Save options for potential stale keepalive retry this[kRetryOptions] = optsWithoutSignal; // initiate connection if (this.agent) { this.agent.addRequest(this, optsWithoutSignal); } else { this._last = true; this.shouldKeepAlive = false; let opts = optsWithoutSignal; if (opts.path || opts.socketPath) { opts = { ...optsWithoutSignal }; if (opts.socketPath) { opts.path = opts.socketPath; } else { opts.path &&= undefined; } } if (typeof opts.createConnection === "function") { const oncreate = once((err, socket) => { if (err) { nextTick(() => emitErrorEvent(this, err)); } else { this.onSocket(socket); } }); try { const newSocket = opts.createConnection(opts, oncreate); if (newSocket) { oncreate(null, newSocket); } } catch (err) { oncreate(err); } } else { this.onSocket(net.createConnection(opts)); } } if (onClientRequestCreatedChannel.hasSubscribers) { onClientRequestCreatedChannel.publish({ request: this, }); } // ---- Inspector: Network.requestWillBeSent ------------------------------ // Fire here so the user-code call site (e.g. `http.get(...)`) is still on // the stack - `op_inspector_emit_protocol_event` captures it as the // `initiator` for DevTools. Any setHeader() the caller does between the // constructor returning and `req.end()` would be missed; the upstream // node:http inspector implementation behaves the same way. inspectorEmitRequestWillBeSent(this, port); } ObjectSetPrototypeOf(ClientRequest.prototype, OutgoingMessage.prototype); ObjectSetPrototypeOf(ClientRequest, OutgoingMessage); ObjectDefineProperty(ClientRequest.prototype, "path", { __proto__: null, get() { return this[kPath]; }, set(value) { const path = String(value); if (INVALID_PATH_REGEX.test(path)) { throw new ERR_UNESCAPED_CHARACTERS("Request path"); } this[kPath] = path; }, configurable: true, enumerable: true, }); ClientRequest.prototype._finish = function _finish() { FunctionPrototypeCall(OutgoingMessage.prototype._finish, this); if (onClientRequestStartChannel.hasSubscribers) { onClientRequestStartChannel.publish({ request: this, }); } }; ClientRequest.prototype._implicitHeader = function _implicitHeader() { if (this._header) { throw new ERR_HTTP_HEADERS_SENT("render"); } // Start OTel client span and inject propagation headers before serialization if (otelState.TRACING_ENABLED && !this[kOtelSpan]) { const span = builtinTracer().startSpan(this.method, { kind: 2 }); // Kind 2 = Client this[kOtelSpan] = span; // Build a context with this span for propagation injection, // without entering it into the async context const spanContext = ContextManager.active().setValue(SPAN_KEY, span); for ( const propagator of new SafeArrayIterator(otelState.PROPAGATORS) ) { propagator.inject(spanContext, this, { set(carrier, key, value) { carrier.setHeader(key, value); }, }); } // Set request attributes const protocol = this.protocol || "http:"; const host = this.getHeader("host") || this.host || "localhost"; const path = this.path || "/"; const fullUrl = `${protocol}//${host}${path}`; try { const parsedUrl = new URL(fullUrl); span.setAttribute("http.request.method", this.method); span.setAttribute("url.full", parsedUrl.href); span.setAttribute( "url.scheme", StringPrototypeSlice(parsedUrl.protocol, 0, -1), ); span.setAttribute("url.path", parsedUrl.pathname); span.setAttribute("url.query", StringPrototypeSlice(parsedUrl.search, 1)); } catch { span.setAttribute("http.request.method", this.method); span.setAttribute("url.full", fullUrl); } } 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; 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 canRetryRequest(req) { return req.reusedSocket && !req.res && req.agent && !req._retrying && req[kRetryData] !== null; } function getRetryDataSize(data, encoding) { if (typeof data === "string") { return Buffer.byteLength(data, encoding || undefined); } // deno-lint-ignore deno-internal/prefer-primordials return data?.byteLength ?? data?.length ?? 0; } function cloneOutputDataForRetry(req) { const retryData = []; let retryDataSize = 0; for (let i = 0; i < req.outputData.length; i++) { const item = req.outputData[i]; retryDataSize += getRetryDataSize(item.data, item.encoding); if (retryDataSize > MAX_RETRY_DATA_SIZE) { req[kRetryData] = null; req[kRetryDataSize] = 0; return; } ArrayPrototypePush(retryData, { data: item.data, encoding: item.encoding, callback: item.callback, }); } req[kRetryData] = retryData; req[kRetryDataSize] = retryDataSize; } // Transparently retry a request on a new connection when the reused // keepalive socket turns out to be stale (server closed it while idle). function maybeRetryRequest(req, socket) { if (!canRetryRequest(req)) return false; req._retrying = true; const agent = req.agent; const retryHeader = req._header; // Clean up parser on the old socket const parser = socket.parser; if (parser) { parser.finish(); freeParser(parser, req, socket); } // Remove listeners installed by tickOnSocket socket.removeListener("close", socketCloseListener); socket.removeListener("error", socketErrorListener); socket.removeListener("data", socketOnData); socket.removeListener("end", socketOnEnd); socket.removeListener("drain", ondrain); if (req.timeoutCb) { socket.removeListener("timeout", req.timeoutCb); } socket.removeListener("timeout", responseOnTimeout); // Remove the stale socket from the agent's active sockets so that // addRequest sees room under maxSockets to create a new connection. // The installListeners onClose handler will still fire after destroy() // and handle totalSocketCount bookkeeping. const retryOpts = req[kRetryOptions]; const name = agent.getName(retryOpts); const sockets = agent.sockets[name]; if (sockets) { const idx = ArrayPrototypeIndexOf(sockets, socket); if (idx !== -1) ArrayPrototypeSplice(sockets, idx, 1); if (!sockets.length) delete agent.sockets[name]; } socket.destroy(); // Reset request state for retry. // Keep finished as-is: if end() was called, _flush() on the new socket // needs to call _finish() to emit 'finish' (which pipeline awaits). req.socket = null; req.parser = null; req._header = null; req._headerSent = false; req.destroyed = false; req._closed = false; // The first attempt set reusedSocket on the stale socket. The retry runs // through addRequest again, which will call agent.reuseSocket() if it // happens to land on another pooled free socket. Otherwise the request // goes to a brand-new connection and the flag must stay false. req.reusedSocket = false; // Restore output data saved before the first flush attempt if (req[kRetryData]) { req.outputData = req[kRetryData]; req[kRetryData] = null; req[kRetryDataSize] = 0; req._header = retryHeader; req._headerSent = true; } // Re-queue through agent to get a fresh socket agent.addRequest(req, retryOpts); return true; } function socketCloseListener() { const socket = this; const req = socket._httpMessage; // Guard against close firing on a socket that has no associated request. if (!req) { return; } const parser = socket.parser; const res = req.res; // Retry on stale keepalive socket before any error/cleanup handling if (!res && maybeRetryRequest(req, socket)) return; // End OTel span on socket close const span = req[kOtelSpan]; if (span) { if (!res || !res.complete) { updateSpanFromError(span, connResetException("socket hang up")); } span.end(); req[kOtelSpan] = null; } req.destroyed = true; if (res) { if (!res.complete) { res.destroy(connResetException("aborted")); } req._closed = true; req.emit("close"); if (!res.aborted && res.readable) { // deno-lint-ignore deno-internal/prefer-primordials res.push(null); } } else { if (!req.socket._hadError) { req.socket._hadError = true; emitErrorEvent(req, 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; if (req) { // Retry on stale keepalive socket before emitting error if (maybeRetryRequest(req, socket)) return; // End OTel span on error const span = req[kOtelSpan]; if (span) { updateSpanFromError(span, err); span.end(); req[kOtelSpan] = null; } socket._hadError = true; emitErrorEvent(req, 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; // Retry on stale keepalive socket (server sent FIN while idle) if (!req.res && maybeRetryRequest(req, socket)) return; if (!req.res && !req.socket._hadError) { req.socket._hadError = true; emitErrorEvent(req, 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 (ObjectPrototypeIsPrototypeOf(ErrorPrototype, ret)) { prepareError(ret, parser, d); freeParser(parser, req, socket); socket.removeListener("data", socketOnData); socket.removeListener("end", socketOnEnd); socket.destroy(); req.socket._hadError = true; emitErrorEvent(req, ret); } else if (parser.incoming?.upgrade) { // Upgrade (if status code 101) or CONNECT 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); // deno-lint-ignore deno-internal/prefer-primordials 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?.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); } // client function parserOnIncomingClient(res, shouldKeepAlive) { const socket = this.socket; const req = socket._httpMessage; if (req.res) { socket.destroy(); if (socket.parser) { socket.parser.incoming = req.res; socket.parser.incoming[kSkipPendingData] = true; } 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; } req.res = res; req[kRetryData] = null; req[kRetryDataSize] = 0; res.req = req; // Emit HttpClient perf entry (at response-header time) const perfStartTime = req[kPerfStartTime]; if (perfStartTime !== undefined) { const host = req.getHeader("host") || req.host || "localhost"; enqueueNodePerformanceEntry({ name: "HttpClient", entryType: "http", startTime: perfStartTime, duration: performance.now() - perfStartTime, detail: { req: { method: req.method, url: `${req.protocol || "http:"}//${host}${req.path || "/"}`, headers: req.getHeaders(), }, res: { statusCode: res.statusCode, statusMessage: res.statusMessage || "", headers: res.headers, }, }, }); } if (onClientResponseFinishChannel.hasSubscribers) { onClientResponseFinishChannel.publish({ request: req, response: res, }); } // ---- Inspector: Network.responseReceived ------------------------------- // Also installs the `res.push` wrapper that emits `Network.dataReceived` // and `Network.loadingFinished` once body bytes start flowing. inspectorEmitResponseReceived(req, res); // Set OTel response attributes const span = req[kOtelSpan]; if (span) { span.setAttribute("http.response.status_code", res.statusCode); if (res.statusCode >= 400) { span.setAttribute("error.type", String(res.statusCode)); span.setStatus({ code: 2 }); } } // Add our listener first, so that we guarantee socket cleanup res.on("end", responseOnEnd); req.on("finish", requestOnFinish); 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; } // client function responseKeepAlive(req) { const socket = req.socket; 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); // There are cases where _handle === null. Avoid those. Passing undefined to // nextTick() will call getDefaultTriggerAsyncId() to retrieve the id. const asyncId = socket._handle ? socket._handle.getAsyncId() : undefined; defaultTriggerAsyncIdScope(asyncId, nextTick, emitFreeNT, req); req.destroyed = true; if (req.res) { // Detach socket from IncomingMessage to avoid destroying the freed // socket in IncomingMessage.destroy(). 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); } // End OTel client span const span = req[kOtelSpan]; if (span) { span.end(); req[kOtelSpan] = null; } req._ended = true; if (!req.shouldKeepAlive) { if (socket.writable) { if (typeof socket.destroySoon === "function") { socket.destroySoon(); } else { socket.end(); } } assert(!socket.writable); } else if (req.writableFinished && !this.aborted) { assert(req.finished); responseKeepAlive(req); } } function responseOnTimeout() { const req = this._httpMessage; if (!req) return; const res = req.res; if (!res) return; res.emit("timeout"); } function requestOnFinish() { const req = this; if (req.shouldKeepAlive && req._ended && !req.destroyed) { 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, ); 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.joinDuplicateHeaders = req.joinDuplicateHeaders; parser.onIncoming = parserOnIncomingClient; socket.on("data", socketOnData); socket.on("end", socketOnEnd); socket.on("close", socketCloseListener); socket.on("drain", ondrain); if ( req.timeout !== undefined || (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) { if (socket && !err && socket.destroyed && socket.errored) { err = socket.errored; } if (socket && !err) { socket._httpMessage = this; socket.on("error", socketErrorListener); } 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 && !socket?._hadError) { emitErrorEvent(req, 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); // Save output data before flushing so it can be replayed on retry // if this reused keepalive socket turns out to be stale. if (canRetryRequest(req) && req.outputData.length > 0) { cloneOutputDataForRetry(req); } req._flush(); } } ClientRequest.prototype._deferToConnect = _deferToConnect; function _deferToConnect(method, arguments_) { const callSocketMethod = () => { if (method) { ReflectApply(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); }; ClientRequest.prototype._recordRetryData = function _recordRetryData( data, encoding, callback, ) { if (!canRetryRequest(this)) return; const dataSize = getRetryDataSize(data, encoding); const retryDataSize = (this[kRetryDataSize] ?? 0) + dataSize; if (retryDataSize > MAX_RETRY_DATA_SIZE) { this[kRetryData] = null; this[kRetryDataSize] = 0; return; } if (this[kRetryData] === null || this[kRetryData] === undefined) { this[kRetryData] = []; } ArrayPrototypePush(this[kRetryData], { data, encoding, callback }); this[kRetryDataSize] = retryDataSize; }; export { ClientRequest }; export default { ClientRequest };