summaryrefslogtreecommitdiff
path: root/ext/node/polyfills/http.ts
diff options
context:
space:
mode:
Diffstat (limited to 'ext/node/polyfills/http.ts')
-rw-r--r--ext/node/polyfills/http.ts1104
1 files changed, 967 insertions, 137 deletions
diff --git a/ext/node/polyfills/http.ts b/ext/node/polyfills/http.ts
index 6f7877742..adc5845b5 100644
--- a/ext/node/polyfills/http.ts
+++ b/ext/node/polyfills/http.ts
@@ -1,23 +1,52 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// import { ReadableStreamPrototype } from "ext:deno_web/06_streams.js";
+
+const core = globalThis.__bootstrap.core;
import { TextEncoder } from "ext:deno_web/08_text_encoding.js";
import { type Deferred, deferred } from "ext:deno_node/_util/async.ts";
-import { _normalizeArgs, ListenOptions, Socket } from "ext:deno_node/net.ts";
+import {
+ _normalizeArgs,
+ // createConnection,
+ ListenOptions,
+ Socket,
+} from "ext:deno_node/net.ts";
import { Buffer } from "ext:deno_node/buffer.ts";
import { ERR_SERVER_NOT_RUNNING } from "ext:deno_node/internal/errors.ts";
import { EventEmitter } from "ext:deno_node/events.ts";
import { nextTick } from "ext:deno_node/_next_tick.ts";
-import { validatePort } from "ext:deno_node/internal/validators.mjs";
import {
+ validateBoolean,
+ validateInteger,
+ validatePort,
+} from "ext:deno_node/internal/validators.mjs";
+import {
+ addAbortSignal,
+ finished,
Readable as NodeReadable,
Writable as NodeWritable,
} from "ext:deno_node/stream.ts";
-import { OutgoingMessage } from "ext:deno_node/_http_outgoing.ts";
-import { Agent } from "ext:deno_node/_http_agent.mjs";
-import { chunkExpression as RE_TE_CHUNKED } from "ext:deno_node/_http_common.ts";
+import {
+ OutgoingMessage,
+ parseUniqueHeadersOption,
+ validateHeaderName,
+} from "ext:deno_node/_http_outgoing.ts";
+import { kOutHeaders } from "ext:deno_node/internal/http.ts";
+import { _checkIsHttpToken as checkIsHttpToken } from "ext:deno_node/_http_common.ts";
+import { Agent, globalAgent } from "ext:deno_node/_http_agent.mjs";
+// import { chunkExpression as RE_TE_CHUNKED } from "ext:deno_node/_http_common.ts";
import { urlToHttpOptions } from "ext:deno_node/internal/url.ts";
+import { kEmptyObject } from "ext:deno_node/internal/util.mjs";
import { constants, TCP } from "ext:deno_node/internal_binding/tcp_wrap.ts";
-import { connResetException } from "ext:deno_node/internal/errors.ts";
+import {
+ connResetException,
+ ERR_HTTP_HEADERS_SENT,
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_HTTP_TOKEN,
+ ERR_INVALID_PROTOCOL,
+ ERR_UNESCAPED_CHARACTERS,
+} from "ext:deno_node/internal/errors.ts";
+import { getTimerDuration } from "ext:deno_node/internal/timers.mjs";
import { serve, upgradeHttpRaw } from "ext:deno_http/00_serve.js";
enum STATUS_CODES {
@@ -218,136 +247,550 @@ export interface RequestOptions {
href?: string;
}
-// TODO(@bartlomieju): Implement ClientRequest methods (e.g. setHeader())
+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;
+}
+
+const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
+const kError = Symbol("kError");
+
+const kUniqueHeaders = Symbol("kUniqueHeaders");
+
/** ClientRequest represents the http(s) request from the client */
-class ClientRequest extends NodeWritable {
+class ClientRequest extends OutgoingMessage {
defaultProtocol = "http:";
- body: null | ReadableStream = null;
- controller: ReadableStreamDefaultController | null = null;
+ aborted = false;
+ destroyed = false;
+ agent: Agent;
+ method: string;
+ maxHeaderSize: number | undefined;
+ insecureHTTPParser: boolean;
+ useChunkedEncodingByDefault: boolean;
+ path: string;
+
constructor(
- public opts: RequestOptions,
- public cb?: (res: IncomingMessageForClient) => void,
+ input: string | URL,
+ options?: RequestOptions,
+ cb?: (res: IncomingMessageForClient) => void,
) {
super();
- }
- // deno-lint-ignore no-explicit-any
- override _write(chunk: any, _enc: string, cb: () => void) {
- if (this.controller) {
- this.controller.enqueue(chunk);
- cb();
- return;
+ if (typeof input === "string") {
+ const urlStr = input;
+ input = urlToHttpOptions(new URL(urlStr));
+ } else if (input instanceof URL) {
+ // url.URL instance
+ input = urlToHttpOptions(input);
+ } else {
+ cb = options;
+ options = input;
+ input = null;
}
- this.body = new ReadableStream({
- start: (controller) => {
- this.controller = controller;
- controller.enqueue(chunk);
- cb();
- },
- });
- }
+ if (typeof options === "function") {
+ cb = options;
+ options = input || kEmptyObject;
+ } else {
+ options = Object.assign(input || {}, options);
+ }
+
+ let agent = options!.agent;
+ const defaultAgent = options!._defaultAgent || globalAgent;
+ if (agent === false) {
+ agent = new defaultAgent.constructor();
+ } else if (agent === null || agent === undefined) {
+ if (typeof options!.createConnection !== "function") {
+ agent = defaultAgent;
+ }
+ // Explicitly pass through this statement as agent will not be used
+ // when createConnection is provided.
+ } 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.exec(path) !== null) {
+ 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 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;
- override async _final() {
- if (this.controller) {
- this.controller.close();
+ if (options!.timeout !== undefined) {
+ this.timeout = getTimerDuration(options.timeout, "timeout");
}
- const body = await this._createBody(this.body, this.opts);
- const client = await this._createCustomClient();
- const opts = {
- body,
- method: this.opts.method,
+ 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) {
+ validateBoolean(insecureHTTPParser, "options.insecureHTTPParser");
+ }
+
+ this.insecureHTTPParser = insecureHTTPParser;
+
+ if (options!.joinDuplicateHeaders !== undefined) {
+ validateBoolean(
+ options!.joinDuplicateHeaders,
+ "options.joinDuplicateHeaders",
+ );
+ }
+
+ this.joinDuplicateHeaders = options!.joinDuplicateHeaders;
+
+ 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;
+ this.port = port;
+ this.hash = options.hash;
+ this.search = options.search;
+ this.auth = options.auth;
+
+ if (this.agent) {
+ // If there is an agent we should default to Connection:keep-alive,
+ // but only if the Agent will actually reuse the connection!
+ // If it's not a keepAlive agent, and the maxSockets==Infinity, then
+ // there's never a case where this socket will actually be reused
+ 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);
+ // Retain for(;;) loop for performance reasons
+ // Refs: https://github.com/nodejs/node/pull/30958
+ 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;
+
+ // For the Host header, ensure that IPv6 addresses are enclosed
+ // in square brackets, as defined by URI formatting
+ // https://tools.ietf.org/html/rfc3986#section-3.2.2
+ 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") && this._header) {
+ throw new ERR_HTTP_HEADERS_SENT("render");
+ }
+ } else {
+ for (const [key, val] of options!.headers) {
+ this.setHeader(key, val);
+ }
+ }
+
+ this[kUniqueHeaders] = parseUniqueHeadersOption(options!.uniqueHeaders);
+
+ let optsWithoutSignal = options as RequestOptions;
+ if (optsWithoutSignal.signal) {
+ optsWithoutSignal = Object.assign({}, options);
+ delete optsWithoutSignal.signal;
+ }
+
+ // initiate connection
+ // TODO(crowlKats): finish this
+ /*if (this.agent) {
+ this.agent.addRequest(this, optsWithoutSignal);
+ } else {
+ // No agent, default to Connection:close.
+ this._last = true;
+ this.shouldKeepAlive = false;
+ if (typeof optsWithoutSignal.createConnection === "function") {
+ const oncreate = once((err, socket) => {
+ if (err) {
+ 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(createConnection(optsWithoutSignal));
+ }
+ }*/
+
+ const url = this._createUrlStrFromOptions();
+
+ const headers = [];
+ for (const key in this[kOutHeaders]) {
+ if (Object.hasOwn(this[kOutHeaders], key)) {
+ const entry = this[kOutHeaders][key];
+ this._processHeader(headers, entry[0], entry[1], false);
+ }
+ }
+
+ const client = this._getClient();
+
+ const req = core.ops.op_node_http_request(
+ this.method,
+ url,
+ headers,
client,
- headers: this.opts.headers,
- signal: this.opts.signal ?? undefined,
- };
- const mayResponse = fetch(this._createUrlStrFromOptions(this.opts), opts)
- .catch((e) => {
- if (e.message.includes("connection closed before message completed")) {
- // Node.js seems ignoring this error
- } else if (e.message.includes("The signal has been aborted")) {
- // Remap this error
- this.emit("error", connResetException("socket hang up"));
+ this.method === "POST" || this.method === "PATCH",
+ );
+
+ this._req = req;
+
+ if (req.requestBodyRid !== null) {
+ const reader = this.stream.getReader();
+ (async () => {
+ let done = false;
+ while (!done) {
+ let val;
+ try {
+ const res = await reader.read();
+ done = res.done;
+ val = res.value;
+ } catch (err) {
+ //if (terminator.aborted) break;
+ // TODO(lucacasonato): propagate error into response body stream
+ this._requestSendError = err;
+ this._requestSendErrorSet = true;
+ break;
+ }
+ if (done) break;
+ try {
+ await core.writeAll(req.requestBodyRid, val);
+ } catch (err) {
+ //if (terminator.aborted) break;
+ await reader.cancel(err);
+ // TODO(lucacasonato): propagate error into response body stream
+ this._requestSendError = err;
+ this._requestSendErrorSet = true;
+ break;
+ }
+ }
+ if (done /*&& !terminator.aborted*/) {
+ try {
+ await core.shutdown(req.requestBodyRid);
+ } catch (err) {
+ // TODO(bartlomieju): fix this conditional
+ // deno-lint-ignore no-constant-condition
+ if (true) {
+ this._requestSendError = err;
+ this._requestSendErrorSet = true;
+ }
+ }
+ }
+ //WeakMapPrototypeDelete(requestBodyReaders, req);
+ core.tryClose(req.requestBodyRid);
+ })();
+ }
+ }
+
+ _getClient(): Deno.HttpClient | undefined {
+ return undefined;
+ }
+
+ onSocket(socket, err) {
+ if (this.destroyed || err) {
+ this.destroyed = true;
+
+ // deno-lint-ignore no-inner-declarations
+ 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 && this.agent && !socket.destroyed) {
+ socket.emit("free");
} else {
- this.emit("error", e);
+ finished(socket.destroy(err || this[kError]), (er) => {
+ if (er?.code === "ERR_STREAM_PREMATURE_CLOSE") {
+ er = null;
+ }
+ _destroy(this, er || err);
+ });
+ return;
}
- return undefined;
- });
+ }
- const res = new IncomingMessageForClient(
- await mayResponse,
- this._createSocket(),
- );
- this.emit("response", res);
- if (client) {
- res.on("end", () => {
- client.close();
- });
+ _destroy(this, err || this[kError]);
+ } else {
+ //tickOnSocket(this, socket);
+ //this._flush();
}
- if (this.opts.timeout != undefined) {
- clearTimeout(this.opts.timeout);
- this.opts.timeout = undefined;
+ }
+
+ // TODO(bartlomieju): use callback here
+ // deno-lint-ignore no-explicit-any
+ end(chunk?: any, encoding?: any, _cb?: any): this {
+ this.finished = true;
+
+ if (chunk !== undefined) {
+ this.write(chunk, encoding);
}
- this.cb?.(res);
+ this.controller.close();
+
+ core.opAsync("op_fetch_send", this._req.requestRid).then((res) => {
+ const incoming = new IncomingMessageForClient(this.socket);
+
+ // TODO(@crowlKats):
+ // incoming.httpVersionMajor = versionMajor;
+ // incoming.httpVersionMinor = versionMinor;
+ // incoming.httpVersion = `${versionMajor}.${versionMinor}`;
+ // incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders ||
+ // parser.joinDuplicateHeaders;
+
+ incoming.url = res.url;
+ incoming.statusCode = res.status;
+ incoming.statusMessage = res.statusText;
+
+ incoming._addHeaderLines(res.headers);
+ incoming._bodyRid = res.responseRid;
+
+ if (this._req.cancelHandleRid !== null) {
+ core.tryClose(this._req.cancelHandleRid);
+ }
+
+ this.emit("response", incoming);
+ }).catch((err) => {
+ if (this._req.cancelHandleRid !== null) {
+ core.tryClose(this._req.cancelHandleRid);
+ }
+
+ if (this._requestSendErrorSet) {
+ // if the request body stream errored, we want to propagate that error
+ // instead of the original error from opFetchSend
+ throw new TypeError("Failed to fetch: request body stream errored", {
+ cause: this._requestSendError,
+ });
+ }
+
+ if (err.message.includes("connection closed before message completed")) {
+ // Node.js seems ignoring this error
+ } else if (err.message.includes("The signal has been aborted")) {
+ // Remap this error
+ this.emit("error", connResetException("socket hang up"));
+ } else {
+ this.emit("error", err);
+ }
+ });
}
+ /*
+ override async _final() {
+ if (this.controller) {
+ this.controller.close();
+ }
+
+ const body = await this._createBody(this.body, this.opts);
+ const client = await this._createCustomClient();
+ const opts = {
+ body,
+ method: this.opts.method,
+ client,
+ headers: this.opts.headers,
+ signal: this.opts.signal ?? undefined,
+ };
+ const mayResponse = fetch(this._createUrlStrFromOptions(this.opts), opts)
+ .catch((e) => {
+ if (e.message.includes("connection closed before message completed")) {
+ // Node.js seems ignoring this error
+ } else if (e.message.includes("The signal has been aborted")) {
+ // Remap this error
+ this.emit("error", connResetException("socket hang up"));
+ } else {
+ this.emit("error", e);
+ }
+ return undefined;
+ });
+
+ const res = new IncomingMessageForClient(
+ await mayResponse,
+ this._createSocket(),
+ );
+ this.emit("response", res);
+ if (client) {
+ res.on("end", () => {
+ client.close();
+ });
+ }
+ if (this.opts.timeout != undefined) {
+ clearTimeout(this.opts.timeout);
+ this.opts.timeout = undefined;
+ }
+ this.cb?.(res);
+ }*/
abort() {
+ if (this.aborted) {
+ return;
+ }
+ this.aborted = true;
+ this.emit("abort");
+ //process.nextTick(emitAbortNT, this);
this.destroy();
}
- async _createBody(
- body: ReadableStream | null,
- opts: RequestOptions,
- ): Promise<Buffer | ReadableStream | null> {
- if (!body) return null;
- if (!opts.headers) return body;
-
- const headers = Object.fromEntries(
- Object.entries(opts.headers).map(([k, v]) => [k.toLowerCase(), v]),
- );
+ // deno-lint-ignore no-explicit-any
+ destroy(err?: any) {
+ if (this.destroyed) {
+ return this;
+ }
+ this.destroyed = true;
- if (
- !RE_TE_CHUNKED.test(headers["transfer-encoding"]) &&
- !Number.isNaN(Number.parseInt(headers["content-length"], 10))
- ) {
- const bufferList: Buffer[] = [];
- for await (const chunk of body) {
- bufferList.push(chunk);
- }
- return Buffer.concat(bufferList);
+ // If we're aborting, we don't care about any more response data.
+ if (this.res) {
+ this.res._dump();
}
- return body;
+ this[kError] = err;
+ this.socket?.destroy(err);
+
+ return this;
}
_createCustomClient(): Promise<Deno.HttpClient | undefined> {
return Promise.resolve(undefined);
}
- _createSocket(): Socket {
- // Note: Creates a dummy socket for the compatibility
- // Sometimes the libraries check some properties of socket
- // e.g. if (!response.socket.authorized) { ... }
- return new Socket({});
- }
-
- _createUrlStrFromOptions(opts: RequestOptions): string {
- if (opts.href) {
- return opts.href;
+ _createUrlStrFromOptions(): string {
+ if (this.href) {
+ return this.href;
}
- const protocol = opts.protocol ?? this.defaultProtocol;
- const auth = opts.auth;
- const host = opts.host ?? opts.hostname ?? "localhost";
- const defaultPort = opts.agent?.defaultPort;
- const port = opts.port ?? defaultPort ?? 80;
- let path = opts.path ?? "/";
+ const protocol = this.protocol ?? this.defaultProtocol;
+ const auth = this.auth;
+ const host = this.host ?? this.hostname ?? "localhost";
+ const hash = this.hash ? `#${this.hash}` : "";
+ const search = this.search ? this.search : "";
+ const defaultPort = this.agent?.defaultPort;
+ const port = this.port ?? defaultPort ?? 80;
+ let path = this.path ?? "/";
if (!path.startsWith("/")) {
path = "/" + path;
}
return `${protocol}//${auth ? `${auth}@` : ""}${host}${
port === 80 ? "" : `:${port}`
- }${path}`;
+ }${path}${search}${hash}`;
}
setTimeout(timeout: number, callback?: () => void) {
@@ -376,56 +819,453 @@ class ClientRequest extends NodeWritable {
}
}, timeout);
}
+
+ _processHeader(headers, key, value, validate) {
+ if (validate) {
+ validateHeaderName(key);
+ }
+
+ // If key is content-disposition and there is content-length
+ // encode the value in latin1
+ // https://www.rfc-editor.org/rfc/rfc6266#section-4.3
+ // Refs: https://github.com/nodejs/node/pull/46528
+ if (isContentDispositionField(key) && this._contentLength) {
+ value = Buffer.from(value, "latin1");
+ }
+
+ if (Array.isArray(value)) {
+ if (
+ (value.length < 2 || !isCookieField(key)) &&
+ (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(key.toLowerCase()))
+ ) {
+ // Retain for(;;) loop for performance reasons
+ // Refs: https://github.com/nodejs/node/pull/30958
+ for (let i = 0; i < value.length; i++) {
+ headers.push([key, value[i]]);
+ }
+ return;
+ }
+ value = value.join("; ");
+ }
+ headers.push([key, value]);
+ }
+}
+
+// isCookieField performs a case-insensitive comparison of a provided string
+// against the word "cookie." As of V8 6.6 this is faster than handrolling or
+// using a case-insensitive RegExp.
+function isCookieField(s) {
+ return s.length === 6 && s.toLowerCase() === "cookie";
+}
+
+function isContentDispositionField(s) {
+ return s.length === 19 &&
+ StringPrototypeToLowerCase(s) === "content-disposition";
}
+const kHeaders = Symbol("kHeaders");
+const kHeadersDistinct = Symbol("kHeadersDistinct");
+const kHeadersCount = Symbol("kHeadersCount");
+const kTrailers = Symbol("kTrailers");
+const kTrailersDistinct = Symbol("kTrailersDistinct");
+const kTrailersCount = Symbol("kTrailersCount");
+
/** IncomingMessage for http(s) client */
export class IncomingMessageForClient extends NodeReadable {
- reader: ReadableStreamDefaultReader | undefined;
- #statusMessage = "";
- constructor(public response: Response | undefined, public socket: Socket) {
+ decoder = new TextDecoder();
+
+ constructor(socket: Socket) {
super();
- this.reader = response?.body?.getReader();
+
+ this._readableState.readingMore = true;
+
+ this.socket = socket;
+
+ this.httpVersionMajor = null;
+ this.httpVersionMinor = null;
+ this.httpVersion = null;
+ this.complete = false;
+ this[kHeaders] = null;
+ this[kHeadersCount] = 0;
+ this.rawHeaders = [];
+ this[kTrailers] = null;
+ this[kTrailersCount] = 0;
+ this.rawTrailers = [];
+ this.joinDuplicateHeaders = false;
+ this.aborted = false;
+
+ this.upgrade = null;
+
+ // request (server) only
+ this.url = "";
+ this.method = null;
+
+ // response (client) only
+ this.statusCode = null;
+ this.statusMessage = null;
+ this.client = socket;
+
+ this._consuming = false;
+ // Flag for when we decide that this message cannot possibly be
+ // read by the user, so there's no point continuing to handle it.
+ this._dumped = false;
}
- override async _read(_size: number) {
- if (this.reader === undefined) {
- this.push(null);
- return;
+ get connection() {
+ return this.socket;
+ }
+
+ set connection(val) {
+ this.socket = val;
+ }
+
+ get headers() {
+ if (!this[kHeaders]) {
+ this[kHeaders] = {};
+
+ const src = this.rawHeaders;
+ const dst = this[kHeaders];
+
+ for (let n = 0; n < this[kHeadersCount]; n += 2) {
+ this._addHeaderLine(src[n + 0], src[n + 1], dst);
+ }
+ }
+ return this[kHeaders];
+ }
+
+ set headers(val) {
+ this[kHeaders] = val;
+ }
+
+ get headersDistinct() {
+ if (!this[kHeadersDistinct]) {
+ this[kHeadersDistinct] = {};
+
+ const src = this.rawHeaders;
+ const dst = this[kHeadersDistinct];
+
+ for (let n = 0; n < this[kHeadersCount]; n += 2) {
+ this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
+ }
+ }
+ return this[kHeadersDistinct];
+ }
+
+ set headersDistinct(val) {
+ this[kHeadersDistinct] = val;
+ }
+
+ get trailers() {
+ if (!this[kTrailers]) {
+ this[kTrailers] = {};
+
+ const src = this.rawTrailers;
+ const dst = this[kTrailers];
+
+ for (let n = 0; n < this[kTrailersCount]; n += 2) {
+ this._addHeaderLine(src[n + 0], src[n + 1], dst);
+ }
}
- try {
- const res = await this.reader.read();
- if (res.done) {
+ return this[kTrailers];
+ }
+
+ set trailers(val) {
+ this[kTrailers] = val;
+ }
+
+ get trailersDistinct() {
+ if (!this[kTrailersDistinct]) {
+ this[kTrailersDistinct] = {};
+
+ const src = this.rawTrailers;
+ const dst = this[kTrailersDistinct];
+
+ for (let n = 0; n < this[kTrailersCount]; n += 2) {
+ this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
+ }
+ }
+ return this[kTrailersDistinct];
+ }
+
+ set trailersDistinct(val) {
+ this[kTrailersDistinct] = val;
+ }
+
+ setTimeout(msecs, callback) {
+ if (callback) {
+ this.on("timeout", callback);
+ }
+ this.socket.setTimeout(msecs);
+ return this;
+ }
+
+ _read(_n) {
+ if (!this._consuming) {
+ this._readableState.readingMore = false;
+ this._consuming = true;
+ }
+
+ const buf = new Uint8Array(16 * 1024);
+
+ core.read(this._bodyRid, buf).then((bytesRead) => {
+ if (bytesRead === 0) {
this.push(null);
- return;
+ } else {
+ this.push(Buffer.from(buf.subarray(0, bytesRead)));
}
- this.push(res.value);
- } catch (e) {
- // deno-lint-ignore no-explicit-any
- this.destroy(e as any);
+ });
+ }
+
+ // It's possible that the socket will be destroyed, and removed from
+ // any messages, before ever calling this. In that case, just skip
+ // it, since something else is destroying this connection anyway.
+ _destroy(err, cb) {
+ if (!this.readableEnded || !this.complete) {
+ this.aborted = true;
+ this.emit("aborted");
+ }
+
+ core.tryClose(this._bodyRid);
+
+ // If aborted and the underlying socket is not already destroyed,
+ // destroy it.
+ // We have to check if the socket is already destroyed because finished
+ // does not call the callback when this method is invoked from `_http_client`
+ // in `test/parallel/test-http-client-spurious-aborted.js`
+ if (this.socket && !this.socket.destroyed && this.aborted) {
+ this.socket.destroy(err);
+ const cleanup = finished(this.socket, (e) => {
+ if (e?.code === "ERR_STREAM_PREMATURE_CLOSE") {
+ e = null;
+ }
+ cleanup();
+ onError(this, e || err, cb);
+ });
+ } else {
+ onError(this, err, cb);
}
}
- get headers() {
- if (this.response) {
- return Object.fromEntries(this.response.headers.entries());
+ _addHeaderLines(headers, n) {
+ if (headers && headers.length) {
+ let dest;
+ if (this.complete) {
+ this.rawTrailers = headers.flat();
+ this[kTrailersCount] = n;
+ dest = this[kTrailers];
+ } else {
+ this.rawHeaders = headers.flat();
+ this[kHeadersCount] = n;
+ dest = this[kHeaders];
+ }
+
+ if (dest) {
+ for (const header of headers) {
+ this._addHeaderLine(header[0], header[1], dest);
+ }
+ }
}
- return {};
}
- get trailers() {
- return {};
+ // Add the given (field, value) pair to the message
+ //
+ // Per RFC2616, section 4.2 it is acceptable to join multiple instances of the
+ // same header with a ', ' if the header in question supports specification of
+ // multiple values this way. The one exception to this is the Cookie header,
+ // which has multiple values joined with a '; ' instead. If a header's values
+ // cannot be joined in either of these ways, we declare the first instance the
+ // winner and drop the second. Extended header fields (those beginning with
+ // 'x-') are always joined.
+ _addHeaderLine(field, value, dest) {
+ field = matchKnownFields(field);
+ const flag = field.charCodeAt(0);
+ if (flag === 0 || flag === 2) {
+ field = field.slice(1);
+ // Make a delimited list
+ if (typeof dest[field] === "string") {
+ dest[field] += (flag === 0 ? ", " : "; ") + value;
+ } else {
+ dest[field] = value;
+ }
+ } else if (flag === 1) {
+ // Array header -- only Set-Cookie at the moment
+ if (dest["set-cookie"] !== undefined) {
+ dest["set-cookie"].push(value);
+ } else {
+ dest["set-cookie"] = [value];
+ }
+ } else if (this.joinDuplicateHeaders) {
+ // RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2
+ // https://github.com/nodejs/node/issues/45699
+ // allow authorization multiple fields
+ // Make a delimited list
+ if (dest[field] === undefined) {
+ dest[field] = value;
+ } else {
+ dest[field] += ", " + value;
+ }
+ } else if (dest[field] === undefined) {
+ // Drop duplicates
+ dest[field] = value;
+ }
}
- get statusCode() {
- return this.response?.status || 0;
+ _addHeaderLineDistinct(field, value, dest) {
+ field = StringPrototypeToLowerCase(field);
+ if (!dest[field]) {
+ dest[field] = [value];
+ } else {
+ dest[field].push(value);
+ }
}
- get statusMessage() {
- return this.#statusMessage || this.response?.statusText || "";
+ // Call this instead of resume() if we want to just
+ // dump all the data to /dev/null
+ _dump() {
+ if (!this._dumped) {
+ this._dumped = true;
+ // If there is buffered data, it may trigger 'data' events.
+ // Remove 'data' event listeners explicitly.
+ this.removeAllListeners("data");
+ this.resume();
+ }
}
+}
- set statusMessage(v: string) {
- this.#statusMessage = v;
+// This function is used to help avoid the lowercasing of a field name if it
+// matches a 'traditional cased' version of a field name. It then returns the
+// lowercased name to both avoid calling toLowerCase() a second time and to
+// indicate whether the field was a 'no duplicates' field. If a field is not a
+// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception
+// to this is the Set-Cookie header which is indicated by a `1` byte flag, since
+// it is an 'array' field and thus is treated differently in _addHeaderLines().
+function matchKnownFields(field, lowercased) {
+ switch (field.length) {
+ case 3:
+ if (field === "Age" || field === "age") return "age";
+ break;
+ case 4:
+ if (field === "Host" || field === "host") return "host";
+ if (field === "From" || field === "from") return "from";
+ if (field === "ETag" || field === "etag") return "etag";
+ if (field === "Date" || field === "date") return "\u0000date";
+ if (field === "Vary" || field === "vary") return "\u0000vary";
+ break;
+ case 6:
+ if (field === "Server" || field === "server") return "server";
+ if (field === "Cookie" || field === "cookie") return "\u0002cookie";
+ if (field === "Origin" || field === "origin") return "\u0000origin";
+ if (field === "Expect" || field === "expect") return "\u0000expect";
+ if (field === "Accept" || field === "accept") return "\u0000accept";
+ break;
+ case 7:
+ if (field === "Referer" || field === "referer") return "referer";
+ if (field === "Expires" || field === "expires") return "expires";
+ if (field === "Upgrade" || field === "upgrade") return "\u0000upgrade";
+ break;
+ case 8:
+ if (field === "Location" || field === "location") {
+ return "location";
+ }
+ if (field === "If-Match" || field === "if-match") {
+ return "\u0000if-match";
+ }
+ break;
+ case 10:
+ if (field === "User-Agent" || field === "user-agent") {
+ return "user-agent";
+ }
+ if (field === "Set-Cookie" || field === "set-cookie") {
+ return "\u0001";
+ }
+ if (field === "Connection" || field === "connection") {
+ return "\u0000connection";
+ }
+ break;
+ case 11:
+ if (field === "Retry-After" || field === "retry-after") {
+ return "retry-after";
+ }
+ break;
+ case 12:
+ if (field === "Content-Type" || field === "content-type") {
+ return "content-type";
+ }
+ if (field === "Max-Forwards" || field === "max-forwards") {
+ return "max-forwards";
+ }
+ break;
+ case 13:
+ if (field === "Authorization" || field === "authorization") {
+ return "authorization";
+ }
+ if (field === "Last-Modified" || field === "last-modified") {
+ return "last-modified";
+ }
+ if (field === "Cache-Control" || field === "cache-control") {
+ return "\u0000cache-control";
+ }
+ if (field === "If-None-Match" || field === "if-none-match") {
+ return "\u0000if-none-match";
+ }
+ break;
+ case 14:
+ if (field === "Content-Length" || field === "content-length") {
+ return "content-length";
+ }
+ break;
+ case 15:
+ if (field === "Accept-Encoding" || field === "accept-encoding") {
+ return "\u0000accept-encoding";
+ }
+ if (field === "Accept-Language" || field === "accept-language") {
+ return "\u0000accept-language";
+ }
+ if (field === "X-Forwarded-For" || field === "x-forwarded-for") {
+ return "\u0000x-forwarded-for";
+ }
+ break;
+ case 16:
+ if (field === "Content-Encoding" || field === "content-encoding") {
+ return "\u0000content-encoding";
+ }
+ if (field === "X-Forwarded-Host" || field === "x-forwarded-host") {
+ return "\u0000x-forwarded-host";
+ }
+ break;
+ case 17:
+ if (field === "If-Modified-Since" || field === "if-modified-since") {
+ return "if-modified-since";
+ }
+ if (field === "Transfer-Encoding" || field === "transfer-encoding") {
+ return "\u0000transfer-encoding";
+ }
+ if (field === "X-Forwarded-Proto" || field === "x-forwarded-proto") {
+ return "\u0000x-forwarded-proto";
+ }
+ break;
+ case 19:
+ if (field === "Proxy-Authorization" || field === "proxy-authorization") {
+ return "proxy-authorization";
+ }
+ if (field === "If-Unmodified-Since" || field === "if-unmodified-since") {
+ return "if-unmodified-since";
+ }
+ break;
+ }
+ if (lowercased) {
+ return "\u0000" + field;
+ }
+ return matchKnownFields(StringPrototypeToLowerCase(field), true);
+}
+
+function onError(self, error, cb) {
+ // This is to keep backward compatible behavior.
+ // An error is emitted only if there are listeners attached to the event.
+ if (self.listenerCount("error") === 0) {
+ cb();
+ } else {
+ cb(error);
}
}
@@ -799,17 +1639,7 @@ export function request(
): ClientRequest;
// deno-lint-ignore no-explicit-any
export function request(...args: any[]) {
- let options = {};
- if (typeof args[0] === "string") {
- options = urlToHttpOptions(new URL(args.shift()));
- } else if (args[0] instanceof URL) {
- options = urlToHttpOptions(args.shift());
- }
- if (args[0] && typeof args[0] !== "function") {
- Object.assign(options, args.shift());
- }
- args.unshift(options);
- return new ClientRequest(args[0], args[1]);
+ return new ClientRequest(args[0], args[1], args[2]);
}
/** Makes a `GET` HTTP request. */