summaryrefslogtreecommitdiff
path: root/ext/node
diff options
context:
space:
mode:
Diffstat (limited to 'ext/node')
-rw-r--r--ext/node/Cargo.toml2
-rw-r--r--ext/node/lib.rs1
-rw-r--r--ext/node/ops/http.rs101
-rw-r--r--ext/node/ops/mod.rs1
-rw-r--r--ext/node/polyfills/_http_outgoing.ts1444
-rw-r--r--ext/node/polyfills/http.ts1104
-rw-r--r--ext/node/polyfills/https.ts64
7 files changed, 1671 insertions, 1046 deletions
diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml
index 00d36107f..9e74c1708 100644
--- a/ext/node/Cargo.toml
+++ b/ext/node/Cargo.toml
@@ -18,6 +18,7 @@ aes.workspace = true
cbc.workspace = true
data-encoding = "2.3.3"
deno_core.workspace = true
+deno_fetch.workspace = true
deno_fs.workspace = true
deno_media_type.workspace = true
deno_npm.workspace = true
@@ -46,6 +47,7 @@ path-clean = "=0.1.0"
pbkdf2 = "0.12.1"
rand.workspace = true
regex.workspace = true
+reqwest.workspace = true
ring.workspace = true
ripemd = "0.1.3"
rsa.workspace = true
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index aed325c93..9f6e68461 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -206,6 +206,7 @@ deno_core::extension!(deno_node,
ops::zlib::op_zlib_write_async,
ops::zlib::op_zlib_init,
ops::zlib::op_zlib_reset,
+ ops::http::op_node_http_request,
op_node_build_os,
ops::require::op_require_init_paths,
ops::require::op_require_node_module_paths<P>,
diff --git a/ext/node/ops/http.rs b/ext/node/ops/http.rs
new file mode 100644
index 000000000..2039fb388
--- /dev/null
+++ b/ext/node/ops/http.rs
@@ -0,0 +1,101 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+use deno_core::error::type_error;
+use deno_core::error::AnyError;
+use deno_core::op;
+use deno_core::url::Url;
+use deno_core::AsyncRefCell;
+use deno_core::ByteString;
+use deno_core::CancelFuture;
+use deno_core::CancelHandle;
+use deno_core::OpState;
+use deno_fetch::get_or_create_client_from_state;
+use deno_fetch::FetchCancelHandle;
+use deno_fetch::FetchRequestBodyResource;
+use deno_fetch::FetchRequestResource;
+use deno_fetch::FetchReturn;
+use deno_fetch::HttpClientResource;
+use deno_fetch::MpscByteStream;
+use reqwest::header::HeaderMap;
+use reqwest::header::HeaderName;
+use reqwest::header::HeaderValue;
+use reqwest::header::CONTENT_LENGTH;
+use reqwest::Body;
+use reqwest::Method;
+
+#[op]
+pub fn op_node_http_request(
+ state: &mut OpState,
+ method: ByteString,
+ url: String,
+ headers: Vec<(ByteString, ByteString)>,
+ client_rid: Option<u32>,
+ has_body: bool,
+) -> Result<FetchReturn, AnyError> {
+ let client = if let Some(rid) = client_rid {
+ let r = state.resource_table.get::<HttpClientResource>(rid)?;
+ r.client.clone()
+ } else {
+ get_or_create_client_from_state(state)?
+ };
+
+ let method = Method::from_bytes(&method)?;
+ let url = Url::parse(&url)?;
+
+ let mut header_map = HeaderMap::new();
+ for (key, value) in headers {
+ let name = HeaderName::from_bytes(&key)
+ .map_err(|err| type_error(err.to_string()))?;
+ let v = HeaderValue::from_bytes(&value)
+ .map_err(|err| type_error(err.to_string()))?;
+
+ header_map.append(name, v);
+ }
+
+ let mut request = client.request(method.clone(), url).headers(header_map);
+
+ let request_body_rid = if has_body {
+ // If no body is passed, we return a writer for streaming the body.
+ let (stream, tx) = MpscByteStream::new();
+
+ request = request.body(Body::wrap_stream(stream));
+
+ let request_body_rid = state.resource_table.add(FetchRequestBodyResource {
+ body: AsyncRefCell::new(tx),
+ cancel: CancelHandle::default(),
+ });
+
+ Some(request_body_rid)
+ } else {
+ // POST and PUT requests should always have a 0 length content-length,
+ // if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
+ if matches!(method, Method::POST | Method::PUT) {
+ request = request.header(CONTENT_LENGTH, HeaderValue::from(0));
+ }
+ None
+ };
+
+ let cancel_handle = CancelHandle::new_rc();
+ let cancel_handle_ = cancel_handle.clone();
+
+ let fut = async move {
+ request
+ .send()
+ .or_cancel(cancel_handle_)
+ .await
+ .map(|res| res.map_err(|err| type_error(err.to_string())))
+ };
+
+ let request_rid = state
+ .resource_table
+ .add(FetchRequestResource(Box::pin(fut)));
+
+ let cancel_handle_rid =
+ state.resource_table.add(FetchCancelHandle(cancel_handle));
+
+ Ok(FetchReturn {
+ request_rid,
+ request_body_rid,
+ cancel_handle_rid: Some(cancel_handle_rid),
+ })
+}
diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs
index 6bab57fb8..2bbf02d34 100644
--- a/ext/node/ops/mod.rs
+++ b/ext/node/ops/mod.rs
@@ -1,6 +1,7 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
pub mod crypto;
+pub mod http;
pub mod idna;
pub mod require;
pub mod v8;
diff --git a/ext/node/polyfills/_http_outgoing.ts b/ext/node/polyfills/_http_outgoing.ts
index 7382be19c..b859d99ca 100644
--- a/ext/node/polyfills/_http_outgoing.ts
+++ b/ext/node/polyfills/_http_outgoing.ts
@@ -10,13 +10,14 @@ import type { Socket } from "ext:deno_node/net.ts";
import {
kNeedDrain,
kOutHeaders,
- utcDate,
+ // utcDate,
} from "ext:deno_node/internal/http.ts";
+import { notImplemented } from "ext:deno_node/_utils.ts";
import { Buffer } from "ext:deno_node/buffer.ts";
import {
_checkInvalidHeaderChar as checkInvalidHeaderChar,
_checkIsHttpToken as checkIsHttpToken,
- chunkExpression as RE_TE_CHUNKED,
+ // chunkExpression as RE_TE_CHUNKED,
} from "ext:deno_node/_http_common.ts";
import {
defaultTriggerAsyncIdScope,
@@ -27,21 +28,22 @@ const { async_id_symbol } = symbols;
import {
ERR_HTTP_HEADERS_SENT,
ERR_HTTP_INVALID_HEADER_VALUE,
- ERR_HTTP_TRAILER_INVALID,
- ERR_INVALID_ARG_TYPE,
- ERR_INVALID_ARG_VALUE,
+ // ERR_HTTP_TRAILER_INVALID,
+ // ERR_INVALID_ARG_TYPE,
+ // ERR_INVALID_ARG_VALUE,
ERR_INVALID_CHAR,
ERR_INVALID_HTTP_TOKEN,
ERR_METHOD_NOT_IMPLEMENTED,
- ERR_STREAM_ALREADY_FINISHED,
+ // ERR_STREAM_ALREADY_FINISHED,
ERR_STREAM_CANNOT_PIPE,
- ERR_STREAM_DESTROYED,
- ERR_STREAM_NULL_VALUES,
- ERR_STREAM_WRITE_AFTER_END,
+ // ERR_STREAM_DESTROYED,
+ // ERR_STREAM_NULL_VALUES,
+ // ERR_STREAM_WRITE_AFTER_END,
hideStackFrames,
} from "ext:deno_node/internal/errors.ts";
import { validateString } from "ext:deno_node/internal/validators.mjs";
-import { isUint8Array } from "ext:deno_node/internal/util/types.ts";
+// import { isUint8Array } from "ext:deno_node/internal/util/types.ts";
+// import { kStreamBaseField } from "ext:deno_node/internal_binding/stream_wrap.ts";
import { debuglog } from "ext:deno_node/internal/util/debuglog.ts";
let debug = debuglog("http", (fn) => {
@@ -54,98 +56,539 @@ const kCorked = Symbol("corked");
const nop = () => {};
-const RE_CONN_CLOSE = /(?:^|\W)close(?:$|\W)/i;
-
-// 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: string) {
- return s.length === 6 && s.toLowerCase() === "cookie";
-}
-
-// deno-lint-ignore no-explicit-any
-export function OutgoingMessage(this: any) {
- Stream.call(this);
-
- // Queue that holds all currently pending data, until the response will be
- // assigned to the socket (until it will its turn in the HTTP pipeline).
- this.outputData = [];
-
- // `outputSize` is an approximate measure of how much data is queued on this
- // response. `_onPendingData` will be invoked to update similar global
- // per-connection counter. That counter will be used to pause/unpause the
- // TCP socket and HTTP Parser and thus handle the backpressure.
- this.outputSize = 0;
-
- this.writable = true;
- this.destroyed = false;
-
- this._last = false;
- this.chunkedEncoding = false;
- this.shouldKeepAlive = true;
- this.maxRequestsOnConnectionReached = false;
- this._defaultKeepAlive = true;
- this.useChunkedEncodingByDefault = true;
- this.sendDate = false;
- this._removedConnection = false;
- this._removedContLen = false;
- this._removedTE = false;
-
- this._contentLength = null;
- this._hasBody = true;
- this._trailer = "";
- this[kNeedDrain] = false;
-
- this.finished = false;
- this._headerSent = false;
- this[kCorked] = 0;
- this._closed = false;
-
- this.socket = null;
- this._header = null;
- this[kOutHeaders] = null;
-
- this._keepAliveTimeout = 0;
-
- this._onPendingData = nop;
-}
-Object.setPrototypeOf(OutgoingMessage.prototype, Stream.prototype);
-Object.setPrototypeOf(OutgoingMessage, Stream);
+export class OutgoingMessage extends Stream {
+ // deno-lint-ignore no-explicit-any
+ outputData: any[];
+ outputSize: number;
+ writable: boolean;
+ destroyed: boolean;
+
+ _last: boolean;
+ chunkedEncoding: boolean;
+ shouldKeepAlive: boolean;
+ maxRequestsOnConnectionReached: boolean;
+ _defaultKeepAlive: boolean;
+ useChunkedEncodingByDefault: boolean;
+ sendDate: boolean;
+ _removedConnection: boolean;
+ _removedContLen: boolean;
+ _removedTE: boolean;
+
+ _contentLength: number | null;
+ _hasBody: boolean;
+ _trailer: string;
+ [kNeedDrain]: boolean;
+
+ finished: boolean;
+ _headerSent: boolean;
+ [kCorked]: number;
+ _closed: boolean;
+
+ // TODO(crowlKats): use it
+ socket: null;
+ // TODO(crowlKats): use it
+ _header: null;
+ [kOutHeaders]: null | Record<string, [string, string]>;
+
+ _keepAliveTimeout: number;
+ _onPendingData: () => void;
+
+ constructor() {
+ super();
+
+ // Queue that holds all currently pending data, until the response will be
+ // assigned to the socket (until it will its turn in the HTTP pipeline).
+ this.outputData = [];
+
+ // `outputSize` is an approximate measure of how much data is queued on this
+ // response. `_onPendingData` will be invoked to update similar global
+ // per-connection counter. That counter will be used to pause/unpause the
+ // TCP socket and HTTP Parser and thus handle the backpressure.
+ this.outputSize = 0;
+
+ this.writable = true;
+ this.destroyed = false;
+
+ this._last = false;
+ this.chunkedEncoding = false;
+ this.shouldKeepAlive = true;
+ this.maxRequestsOnConnectionReached = false;
+ this._defaultKeepAlive = true;
+ this.useChunkedEncodingByDefault = true;
+ this.sendDate = false;
+ this._removedConnection = false;
+ this._removedContLen = false;
+ this._removedTE = false;
+
+ this._contentLength = null;
+ this._hasBody = true;
+ this._trailer = "";
+ this[kNeedDrain] = false;
+
+ this.finished = false;
+ this._headerSent = false;
+ this[kCorked] = 0;
+ this._closed = false;
+
+ this.socket = null;
+ this._header = null;
+ this[kOutHeaders] = null;
+
+ this._keepAliveTimeout = 0;
+
+ this._onPendingData = nop;
+
+ this.stream = new ReadableStream({
+ start: (controller) => {
+ this.controller = controller;
+ },
+ });
+ }
-Object.defineProperty(OutgoingMessage.prototype, "writableFinished", {
- get() {
+ get writableFinished() {
return (
this.finished &&
this.outputSize === 0 &&
(!this.socket || this.socket.writableLength === 0)
);
- },
-});
+ }
-Object.defineProperty(OutgoingMessage.prototype, "writableObjectMode", {
- get() {
+ get writableObjectMode() {
return false;
- },
-});
+ }
-Object.defineProperty(OutgoingMessage.prototype, "writableLength", {
- get() {
+ get writableLength() {
return this.outputSize + (this.socket ? this.socket.writableLength : 0);
- },
-});
+ }
-Object.defineProperty(OutgoingMessage.prototype, "writableHighWaterMark", {
- get() {
+ get writableHighWaterMark() {
return this.socket ? this.socket.writableHighWaterMark : HIGH_WATER_MARK;
- },
-});
+ }
-Object.defineProperty(OutgoingMessage.prototype, "writableCorked", {
- get() {
+ get writableCorked() {
const corked = this.socket ? this.socket.writableCorked : 0;
return corked + this[kCorked];
- },
-});
+ }
+
+ get connection() {
+ return this.socket;
+ }
+
+ set connection(val) {
+ this.socket = val;
+ }
+
+ get writableEnded() {
+ return this.finished;
+ }
+
+ get writableNeedDrain() {
+ return !this.destroyed && !this.finished && this[kNeedDrain];
+ }
+
+ cork() {
+ if (this.socket) {
+ this.socket.cork();
+ } else {
+ this[kCorked]++;
+ }
+ }
+
+ uncork() {
+ if (this.socket) {
+ this.socket.uncork();
+ } else if (this[kCorked]) {
+ this[kCorked]--;
+ }
+ }
+
+ setTimeout(msecs: number, callback?: (...args: unknown[]) => void) {
+ if (callback) {
+ this.on("timeout", callback);
+ }
+
+ if (!this.socket) {
+ // deno-lint-ignore no-explicit-any
+ this.once("socket", function socketSetTimeoutOnConnect(socket: any) {
+ socket.setTimeout(msecs);
+ });
+ } else {
+ this.socket.setTimeout(msecs);
+ }
+ return this;
+ }
+
+ // 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(error: unknown) {
+ if (this.destroyed) {
+ return this;
+ }
+ this.destroyed = true;
+
+ if (this.socket) {
+ this.socket.destroy(error);
+ } else {
+ // deno-lint-ignore no-explicit-any
+ this.once("socket", function socketDestroyOnConnect(socket: any) {
+ socket.destroy(error);
+ });
+ }
+
+ return this;
+ }
+
+ setHeader(name: string, value: string) {
+ if (this._header) {
+ throw new ERR_HTTP_HEADERS_SENT("set");
+ }
+ validateHeaderName(name);
+ validateHeaderValue(name, value);
+
+ let headers = this[kOutHeaders];
+ if (headers === null) {
+ this[kOutHeaders] = headers = Object.create(null);
+ }
+
+ headers[name.toLowerCase()] = [name, value];
+ return this;
+ }
+
+ appendHeader(name, value) {
+ if (this._header) {
+ throw new ERR_HTTP_HEADERS_SENT("append");
+ }
+ validateHeaderName(name);
+ validateHeaderValue(name, value);
+
+ const field = name.toLowerCase();
+ const headers = this[kOutHeaders];
+ if (headers === null || !headers[field]) {
+ return this.setHeader(name, value);
+ }
+
+ // Prepare the field for appending, if required
+ if (!Array.isArray(headers[field][1])) {
+ headers[field][1] = [headers[field][1]];
+ }
+
+ const existingValues = headers[field][1];
+ if (Array.isArray(value)) {
+ for (let i = 0, length = value.length; i < length; i++) {
+ existingValues.push(value[i]);
+ }
+ } else {
+ existingValues.push(value);
+ }
+
+ return this;
+ }
+
+ // Returns a shallow copy of the current outgoing headers.
+ getHeaders() {
+ const headers = this[kOutHeaders];
+ const ret = Object.create(null);
+ if (headers) {
+ const keys = Object.keys(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];
+ const val = headers[key][1];
+ ret[key] = val;
+ }
+ }
+ return ret;
+ }
+
+ hasHeader(name: string) {
+ validateString(name, "name");
+ return this[kOutHeaders] !== null &&
+ !!this[kOutHeaders][name.toLowerCase()];
+ }
+
+ removeHeader(name: string) {
+ validateString(name, "name");
+
+ if (this._header) {
+ throw new ERR_HTTP_HEADERS_SENT("remove");
+ }
+
+ const key = name.toLowerCase();
+
+ switch (key) {
+ case "connection":
+ this._removedConnection = true;
+ break;
+ case "content-length":
+ this._removedContLen = true;
+ break;
+ case "transfer-encoding":
+ this._removedTE = true;
+ break;
+ case "date":
+ this.sendDate = false;
+ break;
+ }
+
+ if (this[kOutHeaders] !== null) {
+ delete this[kOutHeaders][key];
+ }
+ }
+
+ getHeader(name: string) {
+ validateString(name, "name");
+
+ const headers = this[kOutHeaders];
+ if (headers === null) {
+ return;
+ }
+
+ const entry = headers[name.toLowerCase()];
+ return entry && entry[1];
+ }
+
+ // Returns an array of the names of the current outgoing headers.
+ getHeaderNames() {
+ return this[kOutHeaders] !== null ? Object.keys(this[kOutHeaders]) : [];
+ }
+
+ // Returns an array of the names of the current outgoing raw headers.
+ getRawHeaderNames() {
+ const headersMap = this[kOutHeaders];
+ if (headersMap === null) return [];
+
+ const values = Object.values(headersMap);
+ const headers = Array(values.length);
+ // Retain for(;;) loop for performance reasons
+ // Refs: https://github.com/nodejs/node/pull/30958
+ for (let i = 0, l = values.length; i < l; i++) {
+ // deno-lint-ignore no-explicit-any
+ headers[i] = (values as any)[i][0];
+ }
+
+ return headers;
+ }
+
+ controller: ReadableStreamDefaultController;
+ write(
+ chunk: string | Uint8Array | Buffer,
+ encoding: string | null,
+ // TODO(crowlKats): use callback
+ _callback: () => void,
+ ): boolean {
+ if (typeof chunk === "string") {
+ chunk = Buffer.from(chunk, encoding);
+ }
+ if (chunk instanceof Buffer) {
+ chunk = new Uint8Array(chunk.buffer);
+ }
+
+ this.controller.enqueue(chunk);
+
+ return false;
+ }
+
+ // deno-lint-ignore no-explicit-any
+ addTrailers(_headers: any) {
+ // TODO(crowlKats): finish it
+ notImplemented("OutgoingMessage.addTrailers");
+ }
+
+ // deno-lint-ignore no-explicit-any
+ end(chunk: any, encoding: any, _callback: any) {
+ if (typeof chunk === "function") {
+ callback = chunk;
+ chunk = null;
+ encoding = null;
+ } else if (typeof encoding === "function") {
+ callback = encoding;
+ encoding = null;
+ }
+ // TODO(crowlKats): finish
+
+ return this;
+ }
+
+ flushHeaders() {
+ if (!this._header) {
+ this._implicitHeader();
+ }
+
+ // Force-flush the headers.
+ this._send("");
+ }
+
+ pipe() {
+ // OutgoingMessage should be write-only. Piping from it is disabled.
+ this.emit("error", new ERR_STREAM_CANNOT_PIPE());
+ }
+
+ _implicitHeader() {
+ throw new ERR_METHOD_NOT_IMPLEMENTED("_implicitHeader()");
+ }
+
+ _finish() {
+ assert(this.socket);
+ this.emit("prefinish");
+ }
+
+ // This logic is probably a bit confusing. Let me explain a bit:
+ //
+ // In both HTTP servers and clients it is possible to queue up several
+ // outgoing messages. This is easiest to imagine in the case of a client.
+ // Take the following situation:
+ //
+ // req1 = client.request('GET', '/');
+ // req2 = client.request('POST', '/');
+ //
+ // When the user does
+ //
+ // req2.write('hello world\n');
+ //
+ // it's possible that the first request has not been completely flushed to
+ // the socket yet. Thus the outgoing messages need to be prepared to queue
+ // up data internally before sending it on further to the socket's queue.
+ //
+ // This function, outgoingFlush(), is called by both the Server and Client
+ // to attempt to flush any pending messages out to the socket.
+ _flush() {
+ const socket = this.socket;
+
+ if (socket && socket.writable) {
+ // There might be remaining data in this.output; write it out
+ const ret = this._flushOutput(socket);
+
+ if (this.finished) {
+ // This is a queue to the server or client to bring in the next this.
+ this._finish();
+ } else if (ret && this[kNeedDrain]) {
+ this[kNeedDrain] = false;
+ this.emit("drain");
+ }
+ }
+ }
+
+ _flushOutput(socket: Socket) {
+ while (this[kCorked]) {
+ this[kCorked]--;
+ socket.cork();
+ }
+
+ const outputLength = this.outputData.length;
+ if (outputLength <= 0) {
+ return undefined;
+ }
+
+ const outputData = this.outputData;
+ socket.cork();
+ let ret;
+ // Retain for(;;) loop for performance reasons
+ // Refs: https://github.com/nodejs/node/pull/30958
+ for (let i = 0; i < outputLength; i++) {
+ const { data, encoding, callback } = outputData[i];
+ ret = socket.write(data, encoding, callback);
+ }
+ socket.uncork();
+
+ this.outputData = [];
+ this._onPendingData(-this.outputSize);
+ this.outputSize = 0;
+
+ return ret;
+ }
+
+ // This abstract either writing directly to the socket or buffering it.
+ // deno-lint-ignore no-explicit-any
+ _send(data: any, encoding?: string | null, callback?: () => void) {
+ // This is a shameful hack to get the headers and first body chunk onto
+ // the same packet. Future versions of Node are going to take care of
+ // this at a lower level and in a more general way.
+ if (!this._headerSent && this._header !== null) {
+ // `this._header` can be null if OutgoingMessage is used without a proper Socket
+ // See: /test/parallel/test-http-outgoing-message-inheritance.js
+ if (
+ typeof data === "string" &&
+ (encoding === "utf8" || encoding === "latin1" || !encoding)
+ ) {
+ data = this._header + data;
+ } else {
+ const header = this._header;
+ this.outputData.unshift({
+ data: header,
+ encoding: "latin1",
+ callback: null,
+ });
+ this.outputSize += header.length;
+ this._onPendingData(header.length);
+ }
+ this._headerSent = true;
+ }
+ return this._writeRaw(data, encoding, callback);
+ }
+
+ _writeRaw(
+ // deno-lint-ignore no-explicit-any
+ this: any,
+ // deno-lint-ignore no-explicit-any
+ data: any,
+ encoding?: string | null,
+ callback?: () => void,
+ ) {
+ const conn = this.socket;
+ if (conn && conn.destroyed) {
+ // The socket was destroyed. If we're still trying to write to it,
+ // then we haven't gotten the 'close' event yet.
+ return false;
+ }
+
+ if (typeof encoding === "function") {
+ callback = encoding;
+ encoding = null;
+ }
+
+ if (conn && conn._httpMessage === this && conn.writable) {
+ // There might be pending data in the this.output buffer.
+ if (this.outputData.length) {
+ this._flushOutput(conn);
+ }
+ // Directly write to socket.
+ return conn.write(data, encoding, callback);
+ }
+ // Buffer, as long as we're not destroyed.
+ this.outputData.push({ data, encoding, callback });
+ this.outputSize += data.length;
+ this._onPendingData(data.length);
+ return this.outputSize < HIGH_WATER_MARK;
+ }
+
+ _renderHeaders() {
+ if (this._header) {
+ throw new ERR_HTTP_HEADERS_SENT("render");
+ }
+
+ const headersMap = this[kOutHeaders];
+ // deno-lint-ignore no-explicit-any
+ const headers: any = {};
+
+ if (headersMap !== null) {
+ const keys = Object.keys(headersMap);
+ // Retain for(;;) loop for performance reasons
+ // Refs: https://github.com/nodejs/node/pull/30958
+ for (let i = 0, l = keys.length; i < l; i++) {
+ const key = keys[i];
+ headers[headersMap[key][0]] = headersMap[key][1];
+ }
+ }
+ return headers;
+ }
+
+ // deno-lint-ignore no-explicit-any
+ [EE.captureRejectionSymbol](err: any, _event: any) {
+ this.destroy(err);
+ }
+}
Object.defineProperty(OutgoingMessage.prototype, "_headers", {
get: deprecate(
@@ -177,15 +620,6 @@ Object.defineProperty(OutgoingMessage.prototype, "_headers", {
),
});
-Object.defineProperty(OutgoingMessage.prototype, "connection", {
- get: function () {
- return this.socket;
- },
- set: function (val) {
- this.socket = val;
- },
-});
-
Object.defineProperty(OutgoingMessage.prototype, "_headerNames", {
get: deprecate(
// deno-lint-ignore no-explicit-any
@@ -232,373 +666,6 @@ Object.defineProperty(OutgoingMessage.prototype, "_headerNames", {
),
});
-OutgoingMessage.prototype._renderHeaders = function _renderHeaders() {
- if (this._header) {
- throw new ERR_HTTP_HEADERS_SENT("render");
- }
-
- const headersMap = this[kOutHeaders];
- // deno-lint-ignore no-explicit-any
- const headers: any = {};
-
- if (headersMap !== null) {
- const keys = Object.keys(headersMap);
- // Retain for(;;) loop for performance reasons
- // Refs: https://github.com/nodejs/node/pull/30958
- for (let i = 0, l = keys.length; i < l; i++) {
- const key = keys[i];
- headers[headersMap[key][0]] = headersMap[key][1];
- }
- }
- return headers;
-};
-
-OutgoingMessage.prototype.cork = function () {
- if (this.socket) {
- this.socket.cork();
- } else {
- this[kCorked]++;
- }
-};
-
-OutgoingMessage.prototype.uncork = function () {
- if (this.socket) {
- this.socket.uncork();
- } else if (this[kCorked]) {
- this[kCorked]--;
- }
-};
-
-OutgoingMessage.prototype.setTimeout = function setTimeout(
- msecs: number,
- callback?: (...args: unknown[]) => void,
-) {
- if (callback) {
- this.on("timeout", callback);
- }
-
- if (!this.socket) {
- // deno-lint-ignore no-explicit-any
- this.once("socket", function socketSetTimeoutOnConnect(socket: any) {
- socket.setTimeout(msecs);
- });
- } else {
- this.socket.setTimeout(msecs);
- }
- return this;
-};
-
-// 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.
-OutgoingMessage.prototype.destroy = function destroy(error: unknown) {
- if (this.destroyed) {
- return this;
- }
- this.destroyed = true;
-
- if (this.socket) {
- this.socket.destroy(error);
- } else {
- // deno-lint-ignore no-explicit-any
- this.once("socket", function socketDestroyOnConnect(socket: any) {
- socket.destroy(error);
- });
- }
-
- return this;
-};
-
-// This abstract either writing directly to the socket or buffering it.
-OutgoingMessage.prototype._send = function _send(
- // deno-lint-ignore no-explicit-any
- data: any,
- encoding: string | null,
- callback: () => void,
-) {
- // This is a shameful hack to get the headers and first body chunk onto
- // the same packet. Future versions of Node are going to take care of
- // this at a lower level and in a more general way.
- if (!this._headerSent) {
- if (
- typeof data === "string" &&
- (encoding === "utf8" || encoding === "latin1" || !encoding)
- ) {
- data = this._header + data;
- } else {
- const header = this._header;
- this.outputData.unshift({
- data: header,
- encoding: "latin1",
- callback: null,
- });
- this.outputSize += header.length;
- this._onPendingData(header.length);
- }
- this._headerSent = true;
- }
- return this._writeRaw(data, encoding, callback);
-};
-
-OutgoingMessage.prototype._writeRaw = _writeRaw;
-function _writeRaw(
- // deno-lint-ignore no-explicit-any
- this: any,
- // deno-lint-ignore no-explicit-any
- data: any,
- encoding: string | null,
- callback: () => void,
-) {
- const conn = this.socket;
- if (conn && conn.destroyed) {
- // The socket was destroyed. If we're still trying to write to it,
- // then we haven't gotten the 'close' event yet.
- return false;
- }
-
- if (typeof encoding === "function") {
- callback = encoding;
- encoding = null;
- }
-
- if (conn && conn._httpMessage === this && conn.writable) {
- // There might be pending data in the this.output buffer.
- if (this.outputData.length) {
- this._flushOutput(conn);
- }
- // Directly write to socket.
- return conn.write(data, encoding, callback);
- }
- // Buffer, as long as we're not destroyed.
- this.outputData.push({ data, encoding, callback });
- this.outputSize += data.length;
- this._onPendingData(data.length);
- return this.outputSize < HIGH_WATER_MARK;
-}
-
-OutgoingMessage.prototype._storeHeader = _storeHeader;
-// deno-lint-ignore no-explicit-any
-function _storeHeader(this: any, firstLine: any, headers: any) {
- // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n'
- // in the case of response it is: 'HTTP/1.1 200 OK\r\n'
- const state = {
- connection: false,
- contLen: false,
- te: false,
- date: false,
- expect: false,
- trailer: false,
- header: firstLine,
- };
-
- if (headers) {
- if (headers === this[kOutHeaders]) {
- for (const key in headers) {
- if (Object.hasOwn(headers, key)) {
- const entry = headers[key];
- processHeader(this, state, entry[0], entry[1], false);
- }
- }
- } else if (Array.isArray(headers)) {
- if (headers.length && Array.isArray(headers[0])) {
- for (let i = 0; i < headers.length; i++) {
- const entry = headers[i];
- processHeader(this, state, entry[0], entry[1], true);
- }
- } else {
- if (headers.length % 2 !== 0) {
- throw new ERR_INVALID_ARG_VALUE("headers", headers);
- }
-
- for (let n = 0; n < headers.length; n += 2) {
- processHeader(this, state, headers[n + 0], headers[n + 1], true);
- }
- }
- } else {
- for (const key in headers) {
- if (Object.hasOwn(headers, key)) {
- processHeader(this, state, key, headers[key], true);
- }
- }
- }
- }
-
- let { header } = state;
-
- // Date header
- if (this.sendDate && !state.date) {
- header += "Date: " + utcDate() + "\r\n";
- }
-
- // Force the connection to close when the response is a 204 No Content or
- // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked"
- // header.
- //
- // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but
- // node.js used to send out a zero chunk anyway to accommodate clients
- // that don't have special handling for those responses.
- //
- // It was pointed out that this might confuse reverse proxies to the point
- // of creating security liabilities, so suppress the zero chunk and force
- // the connection to close.
- if (
- this.chunkedEncoding && (this.statusCode === 204 ||
- this.statusCode === 304)
- ) {
- debug(
- this.statusCode + " response should not use chunked encoding," +
- " closing connection.",
- );
- this.chunkedEncoding = false;
- this.shouldKeepAlive = false;
- }
-
- // keep-alive logic
- if (this._removedConnection) {
- this._last = true;
- this.shouldKeepAlive = false;
- } else if (!state.connection) {
- const shouldSendKeepAlive = this.shouldKeepAlive &&
- (state.contLen || this.useChunkedEncodingByDefault || this.agent);
- if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) {
- header += "Connection: close\r\n";
- } else if (shouldSendKeepAlive) {
- header += "Connection: keep-alive\r\n";
- if (this._keepAliveTimeout && this._defaultKeepAlive) {
- const timeoutSeconds = Math.floor(this._keepAliveTimeout / 1000);
- header += `Keep-Alive: timeout=${timeoutSeconds}\r\n`;
- }
- } else {
- this._last = true;
- header += "Connection: close\r\n";
- }
- }
-
- if (!state.contLen && !state.te) {
- if (!this._hasBody) {
- // Make sure we don't end the 0\r\n\r\n at the end of the message.
- this.chunkedEncoding = false;
- } else if (!this.useChunkedEncodingByDefault) {
- this._last = true;
- } else if (
- !state.trailer &&
- !this._removedContLen &&
- typeof this._contentLength === "number"
- ) {
- header += "Content-Length: " + this._contentLength + "\r\n";
- } else if (!this._removedTE) {
- header += "Transfer-Encoding: chunked\r\n";
- this.chunkedEncoding = true;
- } else {
- // We should only be able to get here if both Content-Length and
- // Transfer-Encoding are removed by the user.
- // See: test/parallel/test-http-remove-header-stays-removed.js
- debug("Both Content-Length and Transfer-Encoding are removed");
- }
- }
-
- // Test non-chunked message does not have trailer header set,
- // message will be terminated by the first empty line after the
- // header fields, regardless of the header fields present in the
- // message, and thus cannot contain a message body or 'trailers'.
- if (this.chunkedEncoding !== true && state.trailer) {
- throw new ERR_HTTP_TRAILER_INVALID();
- }
-
- this._header = header + "\r\n";
- this._headerSent = false;
-
- // Wait until the first body chunk, or close(), is sent to flush,
- // UNLESS we're sending Expect: 100-continue.
- if (state.expect) this._send("");
-}
-
-function processHeader(
- // deno-lint-ignore no-explicit-any
- self: any,
- // deno-lint-ignore no-explicit-any
- state: any,
- // deno-lint-ignore no-explicit-any
- key: any,
- // deno-lint-ignore no-explicit-any
- value: any,
- // deno-lint-ignore no-explicit-any
- validate: any,
-) {
- if (validate) {
- validateHeaderName(key);
- }
- if (Array.isArray(value)) {
- if (value.length < 2 || !isCookieField(key)) {
- // Retain for(;;) loop for performance reasons
- // Refs: https://github.com/nodejs/node/pull/30958
- for (let i = 0; i < value.length; i++) {
- storeHeader(self, state, key, value[i], validate);
- }
- return;
- }
- value = value.join("; ");
- }
- storeHeader(self, state, key, value, validate);
-}
-
-function storeHeader(
- // deno-lint-ignore no-explicit-any
- self: any,
- // deno-lint-ignore no-explicit-any
- state: any,
- // deno-lint-ignore no-explicit-any
- key: any,
- // deno-lint-ignore no-explicit-any
- value: any,
- // deno-lint-ignore no-explicit-any
- validate: any,
-) {
- if (validate) {
- validateHeaderValue(key, value);
- }
- state.header += key + ": " + value + "\r\n";
- matchHeader(self, state, key, value);
-}
-
-// deno-lint-ignore no-explicit-any
-function matchHeader(self: any, state: any, field: string, value: any) {
- if (field.length < 4 || field.length > 17) {
- return;
- }
- field = field.toLowerCase();
- switch (field) {
- case "connection":
- state.connection = true;
- self._removedConnection = false;
- if (RE_CONN_CLOSE.test(value)) {
- self._last = true;
- } else {
- self.shouldKeepAlive = true;
- }
- break;
- case "transfer-encoding":
- state.te = true;
- self._removedTE = false;
- if (RE_TE_CHUNKED.test(value)) {
- self.chunkedEncoding = true;
- }
- break;
- case "content-length":
- state.contLen = true;
- self._removedContLen = false;
- break;
- case "date":
- case "expect":
- case "trailer":
- state[field] = true;
- break;
- case "keep-alive":
- self._defaultKeepAlive = false;
- break;
- }
-}
-
export const validateHeaderName = hideStackFrames((name) => {
if (typeof name !== "string" || !name || !checkIsHttpToken(name)) {
throw new ERR_INVALID_HTTP_TOKEN("Header name", name);
@@ -615,114 +682,19 @@ export const validateHeaderValue = hideStackFrames((name, value) => {
}
});
-OutgoingMessage.prototype.setHeader = function setHeader(
- name: string,
- value: string,
-) {
- if (this._header) {
- throw new ERR_HTTP_HEADERS_SENT("set");
- }
- validateHeaderName(name);
- validateHeaderValue(name, value);
-
- let headers = this[kOutHeaders];
- if (headers === null) {
- this[kOutHeaders] = headers = Object.create(null);
- }
-
- headers[name.toLowerCase()] = [name, value];
- return this;
-};
-
-OutgoingMessage.prototype.getHeader = function getHeader(name: string) {
- validateString(name, "name");
-
- const headers = this[kOutHeaders];
- if (headers === null) {
- return;
- }
-
- const entry = headers[name.toLowerCase()];
- return entry && entry[1];
-};
-
-// Returns an array of the names of the current outgoing headers.
-OutgoingMessage.prototype.getHeaderNames = function getHeaderNames() {
- return this[kOutHeaders] !== null ? Object.keys(this[kOutHeaders]) : [];
-};
-
-// Returns an array of the names of the current outgoing raw headers.
-OutgoingMessage.prototype.getRawHeaderNames = function getRawHeaderNames() {
- const headersMap = this[kOutHeaders];
- if (headersMap === null) return [];
-
- const values = Object.values(headersMap);
- const headers = Array(values.length);
- // Retain for(;;) loop for performance reasons
- // Refs: https://github.com/nodejs/node/pull/30958
- for (let i = 0, l = values.length; i < l; i++) {
- // deno-lint-ignore no-explicit-any
- headers[i] = (values as any)[i][0];
+export function parseUniqueHeadersOption(headers) {
+ if (!Array.isArray(headers)) {
+ return null;
}
- return headers;
-};
-
-// Returns a shallow copy of the current outgoing headers.
-OutgoingMessage.prototype.getHeaders = function getHeaders() {
- const headers = this[kOutHeaders];
- const ret = Object.create(null);
- if (headers) {
- const keys = Object.keys(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];
- const val = headers[key][1];
- ret[key] = val;
- }
+ const unique = new Set();
+ const l = headers.length;
+ for (let i = 0; i < l; i++) {
+ unique.add(headers[i].toLowerCasee());
}
- return ret;
-};
-OutgoingMessage.prototype.hasHeader = function hasHeader(name: string) {
- validateString(name, "name");
- return this[kOutHeaders] !== null &&
- !!this[kOutHeaders][name.toLowerCase()];
-};
-
-OutgoingMessage.prototype.removeHeader = function removeHeader(name: string) {
- validateString(name, "name");
-
- if (this._header) {
- throw new ERR_HTTP_HEADERS_SENT("remove");
- }
-
- const key = name.toLowerCase();
-
- switch (key) {
- case "connection":
- this._removedConnection = true;
- break;
- case "content-length":
- this._removedContLen = true;
- break;
- case "transfer-encoding":
- this._removedTE = true;
- break;
- case "date":
- this.sendDate = false;
- break;
- }
-
- if (this[kOutHeaders] !== null) {
- delete this[kOutHeaders][key];
- }
-};
-
-OutgoingMessage.prototype._implicitHeader = function _implicitHeader() {
- throw new ERR_METHOD_NOT_IMPLEMENTED("_implicitHeader()");
-};
+ return unique;
+}
Object.defineProperty(OutgoingMessage.prototype, "headersSent", {
configurable: true,
@@ -732,40 +704,13 @@ Object.defineProperty(OutgoingMessage.prototype, "headersSent", {
},
});
-Object.defineProperty(OutgoingMessage.prototype, "writableEnded", {
- get: function () {
- return this.finished;
- },
-});
-
-Object.defineProperty(OutgoingMessage.prototype, "writableNeedDrain", {
- get: function () {
- return !this.destroyed && !this.finished && this[kNeedDrain];
- },
-});
-
+// TODO(bartlomieju): use it
// deno-lint-ignore camelcase
-const crlf_buf = Buffer.from("\r\n");
-OutgoingMessage.prototype.write = function write(
- // deno-lint-ignore no-explicit-any
- chunk: any,
- encoding: string | null,
- callback: () => void,
-) {
- if (typeof encoding === "function") {
- callback = encoding;
- encoding = null;
- }
-
- const ret = write_(this, chunk, encoding, callback, false);
- if (!ret) {
- this[kNeedDrain] = true;
- }
- return ret;
-};
+const _crlf_buf = Buffer.from("\r\n");
+// TODO(bartlomieju): use it
// deno-lint-ignore no-explicit-any
-function onError(msg: any, err: any, callback: any) {
+function _onError(msg: any, err: any, callback: any) {
const triggerAsyncId = msg.socket ? msg.socket[async_id_symbol] : undefined;
defaultTriggerAsyncIdScope(
triggerAsyncId,
@@ -786,314 +731,37 @@ function emitErrorNt(msg: any, err: any, callback: any) {
}
}
-function write_(
+// TODO(bartlomieju): use it
+function _write_(
// deno-lint-ignore no-explicit-any
- msg: any,
+ _msg: any,
// deno-lint-ignore no-explicit-any
- chunk: any,
- encoding: string | null,
+ _chunk: any,
+ _encoding: string | null,
// deno-lint-ignore no-explicit-any
- callback: any,
+ _callback: any,
// deno-lint-ignore no-explicit-any
- fromEnd: any,
+ _fromEnd: any,
) {
- if (typeof callback !== "function") {
- callback = nop;
- }
-
- let len;
- if (chunk === null) {
- throw new ERR_STREAM_NULL_VALUES();
- } else if (typeof chunk === "string") {
- len = Buffer.byteLength(chunk, encoding);
- } else if (isUint8Array(chunk)) {
- len = chunk.length;
- } else {
- throw new ERR_INVALID_ARG_TYPE(
- "chunk",
- ["string", "Buffer", "Uint8Array"],
- chunk,
- );
- }
-
- let err;
- if (msg.finished) {
- err = new ERR_STREAM_WRITE_AFTER_END();
- } else if (msg.destroyed) {
- err = new ERR_STREAM_DESTROYED("write");
- }
-
- if (err) {
- if (!msg.destroyed) {
- onError(msg, err, callback);
- } else {
- // deno-lint-ignore no-explicit-any
- (globalThis as any).process.nextTick(callback, err);
- }
- return false;
- }
-
- if (!msg._header) {
- if (fromEnd) {
- msg._contentLength = len;
- }
- msg._implicitHeader();
- }
-
- if (!msg._hasBody) {
- debug(
- "This type of response MUST NOT have a body. " +
- "Ignoring write() calls.",
- );
- // deno-lint-ignore no-explicit-any
- (globalThis as any).process.nextTick(callback);
- return true;
- }
-
- if (!fromEnd && msg.socket && !msg.socket.writableCorked) {
- msg.socket.cork();
- // deno-lint-ignore no-explicit-any
- (globalThis as any).process.nextTick(connectionCorkNT, msg.socket);
- }
-
- let ret;
- if (msg.chunkedEncoding && chunk.length !== 0) {
- msg._send(len.toString(16), "latin1", null);
- msg._send(crlf_buf, null, null);
- msg._send(chunk, encoding, null);
- ret = msg._send(crlf_buf, null, callback);
- } else {
- ret = msg._send(chunk, encoding, callback);
- }
-
- debug("write ret = " + ret);
- return ret;
+ // TODO(crowlKats): finish
}
+// TODO(bartlomieju): use it
// deno-lint-ignore no-explicit-any
-function connectionCorkNT(conn: any) {
+function _connectionCorkNT(conn: any) {
conn.uncork();
}
+// TODO(bartlomieju): use it
// deno-lint-ignore no-explicit-any
-OutgoingMessage.prototype.addTrailers = function addTrailers(headers: any) {
- this._trailer = "";
- const keys = Object.keys(headers);
- const isArray = Array.isArray(headers);
- // Retain for(;;) loop for performance reasons
- // Refs: https://github.com/nodejs/node/pull/30958
- for (let i = 0, l = keys.length; i < l; i++) {
- let field, value;
- const key = keys[i];
- if (isArray) {
- // deno-lint-ignore no-explicit-any
- field = headers[key as any][0];
- // deno-lint-ignore no-explicit-any
- value = headers[key as any][1];
- } else {
- field = key;
- value = headers[key];
- }
- if (typeof field !== "string" || !field || !checkIsHttpToken(field)) {
- throw new ERR_INVALID_HTTP_TOKEN("Trailer name", field);
- }
- if (checkInvalidHeaderChar(value)) {
- debug('Trailer "%s" contains invalid characters', field);
- throw new ERR_INVALID_CHAR("trailer content", field);
- }
- this._trailer += field + ": " + value + "\r\n";
- }
-};
-
-// deno-lint-ignore no-explicit-any
-function onFinish(outmsg: any) {
+function _onFinish(outmsg: any) {
if (outmsg && outmsg.socket && outmsg.socket._hadError) return;
outmsg.emit("finish");
}
-OutgoingMessage.prototype.end = function end(
- // deno-lint-ignore no-explicit-any
- chunk: any,
- // deno-lint-ignore no-explicit-any
- encoding: any,
- // deno-lint-ignore no-explicit-any
- callback: any,
-) {
- if (typeof chunk === "function") {
- callback = chunk;
- chunk = null;
- encoding = null;
- } else if (typeof encoding === "function") {
- callback = encoding;
- encoding = null;
- }
-
- if (chunk) {
- if (this.finished) {
- onError(
- this,
- new ERR_STREAM_WRITE_AFTER_END(),
- typeof callback !== "function" ? nop : callback,
- );
- return this;
- }
-
- if (this.socket) {
- this.socket.cork();
- }
-
- write_(this, chunk, encoding, null, true);
- } else if (this.finished) {
- if (typeof callback === "function") {
- if (!this.writableFinished) {
- this.on("finish", callback);
- } else {
- callback(new ERR_STREAM_ALREADY_FINISHED("end"));
- }
- }
- return this;
- } else if (!this._header) {
- if (this.socket) {
- this.socket.cork();
- }
-
- this._contentLength = 0;
- this._implicitHeader();
- }
-
- if (typeof callback === "function") {
- this.once("finish", callback);
- }
-
- const finish = onFinish.bind(undefined, this);
-
- if (this._hasBody && this.chunkedEncoding) {
- this._send("0\r\n" + this._trailer + "\r\n", "latin1", finish);
- } else if (!this._headerSent || this.writableLength || chunk) {
- this._send("", "latin1", finish);
- } else {
- // deno-lint-ignore no-explicit-any
- (globalThis as any).process.nextTick(finish);
- }
-
- if (this.socket) {
- // Fully uncork connection on end().
- this.socket._writableState.corked = 1;
- this.socket.uncork();
- }
- this[kCorked] = 0;
-
- this.finished = true;
-
- // There is the first message on the outgoing queue, and we've sent
- // everything to the socket.
- debug("outgoing message end.");
- if (
- this.outputData.length === 0 &&
- this.socket &&
- this.socket._httpMessage === this
- ) {
- this._finish();
- }
-
- return this;
-};
-
-OutgoingMessage.prototype._finish = function _finish() {
- assert(this.socket);
- this.emit("prefinish");
-};
-
-// This logic is probably a bit confusing. Let me explain a bit:
-//
-// In both HTTP servers and clients it is possible to queue up several
-// outgoing messages. This is easiest to imagine in the case of a client.
-// Take the following situation:
-//
-// req1 = client.request('GET', '/');
-// req2 = client.request('POST', '/');
-//
-// When the user does
-//
-// req2.write('hello world\n');
-//
-// it's possible that the first request has not been completely flushed to
-// the socket yet. Thus the outgoing messages need to be prepared to queue
-// up data internally before sending it on further to the socket's queue.
-//
-// This function, outgoingFlush(), is called by both the Server and Client
-// to attempt to flush any pending messages out to the socket.
-OutgoingMessage.prototype._flush = function _flush() {
- const socket = this.socket;
-
- if (socket && socket.writable) {
- // There might be remaining data in this.output; write it out
- const ret = this._flushOutput(socket);
-
- if (this.finished) {
- // This is a queue to the server or client to bring in the next this.
- this._finish();
- } else if (ret && this[kNeedDrain]) {
- this[kNeedDrain] = false;
- this.emit("drain");
- }
- }
-};
-
-OutgoingMessage.prototype._flushOutput = function _flushOutput(socket: Socket) {
- while (this[kCorked]) {
- this[kCorked]--;
- socket.cork();
- }
-
- const outputLength = this.outputData.length;
- if (outputLength <= 0) {
- return undefined;
- }
-
- const outputData = this.outputData;
- socket.cork();
- let ret;
- // Retain for(;;) loop for performance reasons
- // Refs: https://github.com/nodejs/node/pull/30958
- for (let i = 0; i < outputLength; i++) {
- const { data, encoding, callback } = outputData[i];
- ret = socket.write(data, encoding, callback);
- }
- socket.uncork();
-
- this.outputData = [];
- this._onPendingData(-this.outputSize);
- this.outputSize = 0;
-
- return ret;
-};
-
-OutgoingMessage.prototype.flushHeaders = function flushHeaders() {
- if (!this._header) {
- this._implicitHeader();
- }
-
- // Force-flush the headers.
- this._send("");
-};
-
-OutgoingMessage.prototype.pipe = function pipe() {
- // OutgoingMessage should be write-only. Piping from it is disabled.
- this.emit("error", new ERR_STREAM_CANNOT_PIPE());
-};
-
-OutgoingMessage.prototype[EE.captureRejectionSymbol] = function (
- // deno-lint-ignore no-explicit-any
- err: any,
- // deno-lint-ignore no-explicit-any
- _event: any,
-) {
- this.destroy(err);
-};
-
export default {
validateHeaderName,
validateHeaderValue,
+ parseUniqueHeadersOption,
OutgoingMessage,
};
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. */
diff --git a/ext/node/polyfills/https.ts b/ext/node/polyfills/https.ts
index a64e8265a..9261e8c2f 100644
--- a/ext/node/polyfills/https.ts
+++ b/ext/node/polyfills/https.ts
@@ -4,15 +4,11 @@
import { notImplemented } from "ext:deno_node/_utils.ts";
import { urlToHttpOptions } from "ext:deno_node/internal/url.ts";
import {
- Agent as HttpAgent,
ClientRequest,
IncomingMessageForClient as IncomingMessage,
type RequestOptions,
} from "ext:deno_node/http.ts";
-import type { Socket } from "ext:deno_node/net.ts";
-
-export class Agent extends HttpAgent {
-}
+import { Agent as HttpAgent } from "ext:deno_node/_http_agent.mjs";
export class Server {
constructor() {
@@ -53,41 +49,61 @@ export function get(...args: any[]) {
return req;
}
-export const globalAgent = undefined;
+export class Agent extends HttpAgent {
+ constructor(options) {
+ super(options);
+ this.defaultPort = 443;
+ this.protocol = "https:";
+ this.maxCachedSessions = this.options.maxCachedSessions;
+ if (this.maxCachedSessions === undefined) {
+ this.maxCachedSessions = 100;
+ }
+
+ this._sessionCache = {
+ map: {},
+ list: [],
+ };
+ }
+}
+
+const globalAgent = new Agent({
+ keepAlive: true,
+ scheduling: "lifo",
+ timeout: 5000,
+});
+
/** HttpsClientRequest class loosely follows http.ClientRequest class API. */
class HttpsClientRequest extends ClientRequest {
override defaultProtocol = "https:";
- override async _createCustomClient(): Promise<
- Deno.HttpClient | undefined
- > {
+ override _getClient(): Deno.HttpClient | undefined {
if (caCerts === null) {
return undefined;
}
if (caCerts !== undefined) {
return Deno.createHttpClient({ caCerts });
}
- const status = await Deno.permissions.query({
- name: "env",
- variable: "NODE_EXTRA_CA_CERTS",
- });
- if (status.state !== "granted") {
- caCerts = null;
- return undefined;
- }
+ // const status = await Deno.permissions.query({
+ // name: "env",
+ // variable: "NODE_EXTRA_CA_CERTS",
+ // });
+ // if (status.state !== "granted") {
+ // caCerts = null;
+ // return undefined;
+ // }
const certFilename = Deno.env.get("NODE_EXTRA_CA_CERTS");
if (!certFilename) {
caCerts = null;
return undefined;
}
- const caCert = await Deno.readTextFile(certFilename);
+ const caCert = Deno.readTextFileSync(certFilename);
caCerts = [caCert];
return Deno.createHttpClient({ caCerts });
}
- override _createSocket(): Socket {
+ /*override _createSocket(): Socket {
// deno-lint-ignore no-explicit-any
return { authorized: true } as any;
- }
+ }*/
}
/** Makes a request to an https server. */
@@ -107,15 +123,21 @@ export function request(
// deno-lint-ignore no-explicit-any
export function request(...args: any[]) {
let options = {};
+
if (typeof args[0] === "string") {
- options = urlToHttpOptions(new URL(args.shift()));
+ const urlStr = args.shift();
+ options = urlToHttpOptions(new URL(urlStr));
} else if (args[0] instanceof URL) {
options = urlToHttpOptions(args.shift());
}
+
if (args[0] && typeof args[0] !== "function") {
Object.assign(options, args.shift());
}
+
+ options._defaultAgent = globalAgent;
args.unshift(options);
+
return new HttpsClientRequest(args[0], args[1]);
}
export default {