mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 07:28:53 +00:00
Compare commits
1 Commits
dylan/byte
...
chloe/http
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d6609fe4d |
@@ -2749,6 +2749,12 @@ pub const HardcodedModule = enum {
|
||||
@"node:_stream_wrap",
|
||||
@"node:_stream_writable",
|
||||
@"node:_tls_common",
|
||||
@"node:_http_common",
|
||||
@"node:_http_agent",
|
||||
@"node:_http_server",
|
||||
@"node:_http_client",
|
||||
@"node:_http_outgoing",
|
||||
@"node:_http_incoming",
|
||||
/// This is gated behind '--expose-internals'
|
||||
@"bun:internal-for-testing",
|
||||
|
||||
@@ -2827,6 +2833,12 @@ pub const HardcodedModule = enum {
|
||||
.{ "node:_stream_wrap", .@"node:_stream_wrap" },
|
||||
.{ "node:_stream_writable", .@"node:_stream_writable" },
|
||||
.{ "node:_tls_common", .@"node:_tls_common" },
|
||||
.{ "node:_http_common", .@"node:_http_common" },
|
||||
.{ "node:_http_agent", .@"node:_http_agent" },
|
||||
.{ "node:_http_client", .@"node:_http_client" },
|
||||
.{ "node:_http_server", .@"node:_http_server" },
|
||||
.{ "node:_http_incoming", .@"node:_http_incoming" },
|
||||
.{ "node:_http_outgoing", .@"node:_http_outgoing" },
|
||||
|
||||
.{ "node-fetch", HardcodedModule.@"node-fetch" },
|
||||
.{ "isomorphic-fetch", HardcodedModule.@"isomorphic-fetch" },
|
||||
@@ -2922,6 +2934,12 @@ pub const HardcodedModule = enum {
|
||||
nodeEntry("node:wasi"),
|
||||
nodeEntry("node:worker_threads"),
|
||||
nodeEntry("node:zlib"),
|
||||
nodeEntry("node:_http_common"),
|
||||
nodeEntry("node:_http_agent"),
|
||||
nodeEntry("node:_http_client"),
|
||||
nodeEntry("node:_http_server"),
|
||||
nodeEntry("node:_http_incoming"),
|
||||
nodeEntry("node:_http_outgoing"),
|
||||
// New Node.js builtins only resolve from the prefixed one.
|
||||
nodeEntryOnlyPrefix("node:test"),
|
||||
|
||||
@@ -2977,41 +2995,20 @@ pub const HardcodedModule = enum {
|
||||
nodeEntry("wasi"),
|
||||
nodeEntry("worker_threads"),
|
||||
nodeEntry("zlib"),
|
||||
nodeEntry("_http_common"),
|
||||
nodeEntry("_http_agent"),
|
||||
nodeEntry("_http_client"),
|
||||
nodeEntry("_http_server"),
|
||||
nodeEntry("_http_incoming"),
|
||||
nodeEntry("_http_outgoing"),
|
||||
|
||||
// sys is a deprecated alias for util
|
||||
.{ "sys", .{ .path = "node:util", .node_builtin = true } },
|
||||
.{ "node:sys", .{ .path = "node:util", .node_builtin = true } },
|
||||
|
||||
// These are returned in builtinModules, but probably not many
|
||||
// packages use them so we will just alias them.
|
||||
.{ "node:_http_agent", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "node:_http_client", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "node:_http_common", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "node:_http_incoming", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "node:_http_outgoing", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "node:_http_server", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "node:_stream_duplex", .{ .path = "node:_stream_duplex", .node_builtin = true } },
|
||||
.{ "node:_stream_passthrough", .{ .path = "node:_stream_passthrough", .node_builtin = true } },
|
||||
.{ "node:_stream_readable", .{ .path = "node:_stream_readable", .node_builtin = true } },
|
||||
.{ "node:_stream_transform", .{ .path = "node:_stream_transform", .node_builtin = true } },
|
||||
.{ "node:_stream_wrap", .{ .path = "node:_stream_wrap", .node_builtin = true } },
|
||||
.{ "node:_stream_writable", .{ .path = "node:_stream_writable", .node_builtin = true } },
|
||||
// we alias these
|
||||
.{ "node:_tls_wrap", .{ .path = "node:tls", .node_builtin = true } },
|
||||
.{ "node:_tls_common", .{ .path = "node:_tls_common", .node_builtin = true } },
|
||||
.{ "_http_agent", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "_http_client", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "_http_common", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "_http_incoming", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "_http_outgoing", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "_http_server", .{ .path = "node:http", .node_builtin = true } },
|
||||
.{ "_stream_duplex", .{ .path = "node:_stream_duplex", .node_builtin = true } },
|
||||
.{ "_stream_passthrough", .{ .path = "node:_stream_passthrough", .node_builtin = true } },
|
||||
.{ "_stream_readable", .{ .path = "node:_stream_readable", .node_builtin = true } },
|
||||
.{ "_stream_transform", .{ .path = "node:_stream_transform", .node_builtin = true } },
|
||||
.{ "_stream_wrap", .{ .path = "node:_stream_wrap", .node_builtin = true } },
|
||||
.{ "_stream_writable", .{ .path = "node:_stream_writable", .node_builtin = true } },
|
||||
.{ "_tls_wrap", .{ .path = "node:tls", .node_builtin = true } },
|
||||
.{ "_tls_common", .{ .path = "node:_tls_common", .node_builtin = true } },
|
||||
};
|
||||
|
||||
const bun_extra_alias_kvs = [_]struct { string, Alias }{
|
||||
|
||||
@@ -12,7 +12,6 @@ import fs from "fs";
|
||||
import { mkdir, writeFile } from "fs/promises";
|
||||
import { builtinModules } from "node:module";
|
||||
import path from "path";
|
||||
import ErrorCode from "../bun.js/bindings/ErrorCode";
|
||||
import { sliceSourceCode } from "./builtin-parser";
|
||||
import { createAssertClientJS, createLogClientJS } from "./client-js";
|
||||
import { getJS2NativeCPP, getJS2NativeZig } from "./generate-js2native";
|
||||
@@ -92,22 +91,61 @@ for (let i = 0; i < nativeStartIndex; i++) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: there is no reason this cannot be converted automatically.
|
||||
// Support some ES import statements.
|
||||
// import { ... } from '...' -> `const { ... } = require('...')`
|
||||
const scannedImports = t.scanImports(input);
|
||||
for (const imp of scannedImports) {
|
||||
if (imp.kind === "import-statement") {
|
||||
var isBuiltin = true;
|
||||
try {
|
||||
if (!builtinModules.includes(imp.path)) {
|
||||
requireTransformer(imp.path, moduleList[i]);
|
||||
}
|
||||
} catch {
|
||||
isBuiltin = false;
|
||||
const result = requireTransformer(imp.path, moduleList[i]);
|
||||
function escapeRegExp(str: string) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
if (isBuiltin) {
|
||||
// Replace the import with a require
|
||||
let called = false;
|
||||
const regexp = new RegExp(
|
||||
`import\\s+(type\\s+)?([a-zA-Z0-9_$]+|{[^}]*}|\\*\\s*as\\s+([a-zA-Z0-9_$]+))\\s+from\\s+['"]${escapeRegExp(JSON.stringify(imp.path).slice(1, -1))}['"];?`,
|
||||
);
|
||||
input = input.replace(
|
||||
regexp,
|
||||
(_, type, clause, star) => {
|
||||
called = true;
|
||||
if (type) return '';
|
||||
let decl;
|
||||
if (clause[0] === '{') {
|
||||
// convert the ES import clause into a destructuring assignment
|
||||
const items = clause.slice(1, -1)
|
||||
.split(',')
|
||||
.map(x => x.trim())
|
||||
.map(item => {
|
||||
if (!item) return null;
|
||||
if (item.startsWith('type')) {
|
||||
return null;
|
||||
}
|
||||
if (item.includes(' as ')) {
|
||||
const [name, as] = item.split(' as ').map(x => x.trim());
|
||||
return `${name}: ${as}`;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
decl = `{ ${items} }`;
|
||||
} else if (star) {
|
||||
decl = star;
|
||||
} else {
|
||||
decl = clause;
|
||||
}
|
||||
return 'const ' + decl + ' = ' + result + ';\n';
|
||||
},
|
||||
);
|
||||
if (!called) {
|
||||
const template = JSON.stringify(imp.path);
|
||||
throw new Error(
|
||||
`Cannot use ESM import statement within builtin modules. Use require("${imp.path}") instead. See src/js/README.md`,
|
||||
`Only a subset of ESM imports are supported in builtin modules.
|
||||
- 'import namespace from ${template};'
|
||||
- 'import * as namespace from ${template};
|
||||
- 'import { name, func } from ${template};`,
|
||||
// Or, the above regular expression is wrong.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@ export const function_replacements = [
|
||||
"$isPromiseRejected",
|
||||
"$isPromisePending",
|
||||
"$bindgenFn",
|
||||
"$unwrap",
|
||||
];
|
||||
const function_regexp = new RegExp(`__intrinsic__(${function_replacements.join("|").replaceAll("$", "")})`);
|
||||
|
||||
@@ -222,6 +223,33 @@ export function applyReplacements(src: string, length: number) {
|
||||
rest2,
|
||||
true,
|
||||
];
|
||||
} else if (name === "unwrap") {
|
||||
const checkSlice = sliceSourceCode(rest, true, undefined, true);
|
||||
let rest2 = checkSlice.rest;
|
||||
let extraArgs = "";
|
||||
if (checkSlice.result.at(-1) === ",") {
|
||||
const sliced = sliceSourceCode("(" + rest2.slice(1), true, undefined, false);
|
||||
extraArgs = ", " + sliced.result.slice(1, -1);
|
||||
rest2 = sliced.rest;
|
||||
}
|
||||
return [
|
||||
slice.slice(0, match.index) +
|
||||
"!(IS_BUN_DEVELOPMENT?($assert(__intrinsic__lazy.temp=(" +
|
||||
checkSlice.result.slice(1, -1) +
|
||||
")," +
|
||||
JSON.stringify(
|
||||
checkSlice.result
|
||||
.slice(1, -1)
|
||||
.replace(/__intrinsic__/g, "$")
|
||||
.trim(),
|
||||
) +
|
||||
extraArgs +
|
||||
"),__intrinsic__lazy.temp):(" +
|
||||
checkSlice.result.slice(1, -1) +
|
||||
"))",
|
||||
rest2,
|
||||
true,
|
||||
];
|
||||
} else if (["zig", "cpp", "newZigFunction", "newCppFunction"].includes(name)) {
|
||||
const kind = name.includes("ig") ? "zig" : "cpp";
|
||||
const is_create_fn = name.startsWith("new");
|
||||
|
||||
19
src/js/builtins.d.ts
vendored
19
src/js/builtins.d.ts
vendored
@@ -20,6 +20,25 @@ declare function $debug(...args: any[]): void;
|
||||
* @note gets removed in release builds. Do not put code with side effects in the `check`.
|
||||
*/
|
||||
declare function $assert(check: any, ...message: any[]): asserts check;
|
||||
/**
|
||||
* Assert that a value is not null or undefined. Returns the unwrapped value.
|
||||
*
|
||||
* The purpose of this builtin is to satisfy the TypeScript type checker in
|
||||
* places where it sees `T | null` but you know for sure it is non-null. For
|
||||
* example, a private variable whose initialization happens based on another
|
||||
* field's state. Unlike the postfix `!` operator in TypeScript (non-null assert),
|
||||
* these will be verified at runtime, with errors appearing much more loudly.
|
||||
*
|
||||
* $unwrap is a preprocessor macro that only runs in debug mode. In release, it is
|
||||
* as if you just wrote the value. In debug, $unwrap(VALUE) is equivalent to:
|
||||
*
|
||||
* function $unwrap(value) {
|
||||
* let temp = VALUE;
|
||||
* if (!temp) throw new Error("Unwrap of " + temp);
|
||||
* return temp;
|
||||
* }
|
||||
*/
|
||||
declare function $unwrap<T>(value: T | null | undefined): T;
|
||||
|
||||
/** Asserts the input is a promise. Returns `true` if the promise is resolved */
|
||||
declare function $isPromiseFulfilled(promise: Promise<any>): boolean;
|
||||
|
||||
385
src/js/internal/http/share.ts
Normal file
385
src/js/internal/http/share.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import type { Server, OutgoingMessage } from "node:http";
|
||||
// NOTE: This cannot be made private since there are public packages that rely on this.
|
||||
const kServer = Symbol.for("::bunternal::");
|
||||
const { Duplex } = require("node:stream");
|
||||
const { kAutoDestroyed } = require("internal/shared");
|
||||
|
||||
export const enum ClientRequestEmitState {
|
||||
socket = 1,
|
||||
prefinish = 2,
|
||||
finish = 3,
|
||||
response = 4,
|
||||
}
|
||||
|
||||
export const enum NodeHTTPResponseAbortEvent {
|
||||
none = 0,
|
||||
abort = 1,
|
||||
timeout = 2,
|
||||
}
|
||||
|
||||
export const enum NodeHTTPIncomingRequestType {
|
||||
FetchRequest,
|
||||
FetchResponse,
|
||||
NodeHTTPResponse,
|
||||
}
|
||||
|
||||
export const enum NodeHTTPHeaderState {
|
||||
none,
|
||||
assigned,
|
||||
sent,
|
||||
}
|
||||
|
||||
export const enum NodeHTTPBodyReadState {
|
||||
none,
|
||||
pending = 1 << 1,
|
||||
done = 1 << 2,
|
||||
hasBufferedDataDuringPause = 1 << 3,
|
||||
}
|
||||
|
||||
/** Must be kept in sync with NodeHTTPResponse.Flags */
|
||||
export const enum NodeHTTPResponseFlags {
|
||||
socket_closed = 1 << 0,
|
||||
request_has_completed = 1 << 1,
|
||||
closed_or_completed = socket_closed | request_has_completed,
|
||||
}
|
||||
|
||||
export const kEmptyObject = Object.freeze(Object.create(null));
|
||||
|
||||
/** used for pretending to emit events in the right order */
|
||||
export const kEmitState = Symbol("emitState");
|
||||
export const kHeaderState= Symbol("headerState");
|
||||
export const abortedSymbol = Symbol("aborted");
|
||||
export const bodyStreamSymbol = Symbol("bodyStream");
|
||||
export const closedSymbol = Symbol("closed");
|
||||
export const controllerSymbol = Symbol("controller");
|
||||
export const runSymbol = Symbol("run");
|
||||
export const deferredSymbol = Symbol("deferred");
|
||||
export const eofInProgress = Symbol("eofInProgress");
|
||||
export const fakeSocketSymbol = Symbol("fakeSocket");
|
||||
export const firstWriteSymbol = Symbol("firstWrite");
|
||||
export const headersSymbol = Symbol("headers");
|
||||
export const isTlsSymbol = Symbol("is_tls");
|
||||
export const kClearTimeout = Symbol("kClearTimeout");
|
||||
export const kfakeSocket = Symbol("kfakeSocket");
|
||||
export const kHandle = Symbol("handle");
|
||||
export const kRealListen = Symbol("kRealListen");
|
||||
export const noBodySymbol = Symbol("noBody");
|
||||
export const optionsSymbol = Symbol("options");
|
||||
export const reqSymbol = Symbol("req");
|
||||
export const timeoutTimerSymbol = Symbol("timeoutTimer");
|
||||
export const tlsSymbol = Symbol("tls");
|
||||
export const typeSymbol = Symbol("type");
|
||||
export const webRequestOrResponse = Symbol("FetchAPI");
|
||||
export const statusCodeSymbol = Symbol("statusCode");
|
||||
export const kEndCalled = Symbol.for("kEndCalled");
|
||||
export const kAbortController = Symbol.for("kAbortController");
|
||||
export const statusMessageSymbol = Symbol("statusMessage");
|
||||
export const serverSymbol = Symbol.for("::bunternal::");
|
||||
export const kPendingCallbacks = Symbol("pendingCallbacks");
|
||||
export const kRequest = Symbol("request");
|
||||
export const kCloseCallback = Symbol("closeCallback");
|
||||
export const kPath = Symbol("path");
|
||||
export const kPort = Symbol("port");
|
||||
export const kMethod = Symbol("method");
|
||||
export const kHost = Symbol("host");
|
||||
export const kProtocol = Symbol("protocol");
|
||||
export const kAgent = Symbol("agent");
|
||||
export const kFetchRequest = Symbol("fetchRequest");
|
||||
export const kTls = Symbol("tls");
|
||||
export const kUseDefaultPort = Symbol("useDefaultPort");
|
||||
export const kBodyChunks = Symbol("bodyChunks");
|
||||
export const kRes = Symbol("res");
|
||||
export const kUpgradeOrConnect = Symbol("upgradeOrConnect");
|
||||
export const kParser = Symbol("parser");
|
||||
export const kMaxHeadersCount = Symbol("maxHeadersCount");
|
||||
export const kReusedSocket = Symbol("reusedSocket");
|
||||
export const kTimeoutTimer = Symbol("timeoutTimer");
|
||||
export const kOptions = Symbol("options");
|
||||
export const kSocketPath = Symbol("socketPath");
|
||||
export const kSignal = Symbol("signal");
|
||||
export const kMaxHeaderSize = Symbol("maxHeaderSize");
|
||||
export const kJoinDuplicateHeaders = Symbol("joinDuplicateHeaders");
|
||||
|
||||
export const {
|
||||
getHeader,
|
||||
setHeader,
|
||||
assignHeaders: assignHeadersFast,
|
||||
assignEventCallback,
|
||||
setRequestTimeout,
|
||||
setServerIdleTimeout,
|
||||
Response,
|
||||
Request,
|
||||
Headers,
|
||||
Blob,
|
||||
headersTuple,
|
||||
drainMicrotasks,
|
||||
} = $cpp("NodeHTTP.cpp", "createNodeHTTPInternalBinding") as {
|
||||
getHeader: (headers: Headers, name: string) => string | undefined;
|
||||
setHeader: (headers: Headers, name: string, value: string) => void;
|
||||
assignHeaders: (object: any, req: Request, headersTuple: any) => boolean;
|
||||
assignEventCallback: (req: Request, callback: (event: number) => void) => void;
|
||||
setRequestTimeout: (req: Request, timeout: number) => void;
|
||||
setServerIdleTimeout: (server: any, timeout: number) => void;
|
||||
Response: (typeof globalThis)["Response"];
|
||||
Request: (typeof globalThis)["Request"];
|
||||
Headers: (typeof globalThis)["Headers"];
|
||||
Blob: (typeof globalThis)["Blob"];
|
||||
headersTuple: any;
|
||||
drainMicrotasks: () => void;
|
||||
};
|
||||
|
||||
export const kFakeSocket = Symbol("kFakeSocket");
|
||||
export const kInternalSocketData = Symbol.for("::bunternal::");
|
||||
|
||||
type FakeSocket = InstanceType<typeof FakeSocket>;
|
||||
export const FakeSocket = class Socket extends Duplex {
|
||||
[kInternalSocketData]!: [typeof Server, typeof OutgoingMessage, typeof Request];
|
||||
bytesRead = 0;
|
||||
bytesWritten = 0;
|
||||
connecting = false;
|
||||
timeout = 0;
|
||||
isServer = false;
|
||||
|
||||
#address;
|
||||
address() {
|
||||
// Call server.requestIP() without doing any property getter twice.
|
||||
var internalData;
|
||||
return (this.#address ??=
|
||||
(internalData = this[kInternalSocketData])?.[0]?.[serverSymbol].requestIP(internalData[2]) ?? {});
|
||||
}
|
||||
|
||||
get bufferSize() {
|
||||
return this.writableLength;
|
||||
}
|
||||
|
||||
connect(port, host, connectListener) {
|
||||
return this;
|
||||
}
|
||||
|
||||
_destroy(err, callback) {
|
||||
const socketData = this[kInternalSocketData];
|
||||
if (!socketData) return; // sometimes 'this' is Socket not FakeSocket
|
||||
if (!socketData[1]["req"][kAutoDestroyed]) socketData[1].end();
|
||||
}
|
||||
|
||||
_final(callback) {}
|
||||
|
||||
get localAddress() {
|
||||
return this.address() ? "127.0.0.1" : undefined;
|
||||
}
|
||||
|
||||
get localFamily() {
|
||||
return "IPv4";
|
||||
}
|
||||
|
||||
get localPort() {
|
||||
return 80;
|
||||
}
|
||||
|
||||
get pending() {
|
||||
return this.connecting;
|
||||
}
|
||||
|
||||
_read(size) {}
|
||||
|
||||
get readyState() {
|
||||
if (this.connecting) return "opening";
|
||||
if (this.readable) {
|
||||
return this.writable ? "open" : "readOnly";
|
||||
} else {
|
||||
return this.writable ? "writeOnly" : "closed";
|
||||
}
|
||||
}
|
||||
|
||||
ref() {
|
||||
return this;
|
||||
}
|
||||
|
||||
get remoteAddress() {
|
||||
return this.address()?.address;
|
||||
}
|
||||
|
||||
set remoteAddress(val) {
|
||||
// initialize the object so that other properties wouldn't be lost
|
||||
this.address().address = val;
|
||||
}
|
||||
|
||||
get remotePort() {
|
||||
return this.address()?.port;
|
||||
}
|
||||
|
||||
set remotePort(val) {
|
||||
// initialize the object so that other properties wouldn't be lost
|
||||
this.address().port = val;
|
||||
}
|
||||
|
||||
get remoteFamily() {
|
||||
return this.address()?.family;
|
||||
}
|
||||
|
||||
set remoteFamily(val) {
|
||||
// initialize the object so that other properties wouldn't be lost
|
||||
this.address().family = val;
|
||||
}
|
||||
|
||||
resetAndDestroy() {}
|
||||
|
||||
setKeepAlive(enable = false, initialDelay = 0) {}
|
||||
|
||||
setNoDelay(noDelay = true) {
|
||||
return this;
|
||||
}
|
||||
|
||||
setTimeout(timeout, callback) {
|
||||
const socketData = this[kInternalSocketData];
|
||||
if (!socketData) return; // sometimes 'this' is Socket not FakeSocket
|
||||
|
||||
const [server, http_res, req] = socketData;
|
||||
http_res?.req?.setTimeout(timeout, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
unref() {
|
||||
return this;
|
||||
}
|
||||
|
||||
_write(chunk, encoding, callback) {}
|
||||
};
|
||||
|
||||
export class ConnResetException extends Error {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
this.code = "ECONNRESET";
|
||||
this.name = "ConnResetException";
|
||||
}
|
||||
}
|
||||
|
||||
function emitErrorNt(msg, err, callback) {
|
||||
if ($isCallable(callback)) {
|
||||
callback(err);
|
||||
}
|
||||
if ($isCallable(msg.emit) && !msg.destroyed) {
|
||||
msg.emit("error", err);
|
||||
}
|
||||
}
|
||||
|
||||
export function hasServerResponseFinished(self, chunk, callback) {
|
||||
const finished = self.finished;
|
||||
|
||||
if (chunk) {
|
||||
const destroyed = self.destroyed;
|
||||
|
||||
if (finished || destroyed) {
|
||||
let err;
|
||||
if (finished) {
|
||||
err = $ERR_STREAM_WRITE_AFTER_END();
|
||||
} else if (destroyed) {
|
||||
err = $ERR_STREAM_DESTROYED("Stream is destroyed");
|
||||
}
|
||||
|
||||
if (!destroyed) {
|
||||
process.nextTick(emitErrorNt, self, err, callback);
|
||||
} else if ($isCallable(callback)) {
|
||||
process.nextTick(callback, err);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} else if (finished) {
|
||||
if ($isCallable(callback)) {
|
||||
if (!self.writableFinished) {
|
||||
self.on("finish", callback);
|
||||
} else {
|
||||
callback($ERR_STREAM_ALREADY_FINISHED("end"));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
let isNextIncomingMessageHTTPSState = false;
|
||||
export function swapIsNextIncomingMessageHTTPS(newValue) {
|
||||
const oldValue = isNextIncomingMessageHTTPSState;
|
||||
isNextIncomingMessageHTTPSState = newValue;
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
export const STATUS_CODES = {
|
||||
100: 'Continue', // RFC 7231 6.2.1
|
||||
101: 'Switching Protocols', // RFC 7231 6.2.2
|
||||
102: 'Processing', // RFC 2518 10.1 (obsoleted by RFC 4918)
|
||||
103: 'Early Hints', // RFC 8297 2
|
||||
200: 'OK', // RFC 7231 6.3.1
|
||||
201: 'Created', // RFC 7231 6.3.2
|
||||
202: 'Accepted', // RFC 7231 6.3.3
|
||||
203: 'Non-Authoritative Information', // RFC 7231 6.3.4
|
||||
204: 'No Content', // RFC 7231 6.3.5
|
||||
205: 'Reset Content', // RFC 7231 6.3.6
|
||||
206: 'Partial Content', // RFC 7233 4.1
|
||||
207: 'Multi-Status', // RFC 4918 11.1
|
||||
208: 'Already Reported', // RFC 5842 7.1
|
||||
226: 'IM Used', // RFC 3229 10.4.1
|
||||
300: 'Multiple Choices', // RFC 7231 6.4.1
|
||||
301: 'Moved Permanently', // RFC 7231 6.4.2
|
||||
302: 'Found', // RFC 7231 6.4.3
|
||||
303: 'See Other', // RFC 7231 6.4.4
|
||||
304: 'Not Modified', // RFC 7232 4.1
|
||||
305: 'Use Proxy', // RFC 7231 6.4.5
|
||||
307: 'Temporary Redirect', // RFC 7231 6.4.7
|
||||
308: 'Permanent Redirect', // RFC 7238 3
|
||||
400: 'Bad Request', // RFC 7231 6.5.1
|
||||
401: 'Unauthorized', // RFC 7235 3.1
|
||||
402: 'Payment Required', // RFC 7231 6.5.2
|
||||
403: 'Forbidden', // RFC 7231 6.5.3
|
||||
404: 'Not Found', // RFC 7231 6.5.4
|
||||
405: 'Method Not Allowed', // RFC 7231 6.5.5
|
||||
406: 'Not Acceptable', // RFC 7231 6.5.6
|
||||
407: 'Proxy Authentication Required', // RFC 7235 3.2
|
||||
408: 'Request Timeout', // RFC 7231 6.5.7
|
||||
409: 'Conflict', // RFC 7231 6.5.8
|
||||
410: 'Gone', // RFC 7231 6.5.9
|
||||
411: 'Length Required', // RFC 7231 6.5.10
|
||||
412: 'Precondition Failed', // RFC 7232 4.2
|
||||
413: 'Payload Too Large', // RFC 7231 6.5.11
|
||||
414: 'URI Too Long', // RFC 7231 6.5.12
|
||||
415: 'Unsupported Media Type', // RFC 7231 6.5.13
|
||||
416: 'Range Not Satisfiable', // RFC 7233 4.4
|
||||
417: 'Expectation Failed', // RFC 7231 6.5.14
|
||||
418: 'I\'m a Teapot', // RFC 7168 2.3.3
|
||||
421: 'Misdirected Request', // RFC 7540 9.1.2
|
||||
422: 'Unprocessable Entity', // RFC 4918 11.2
|
||||
423: 'Locked', // RFC 4918 11.3
|
||||
424: 'Failed Dependency', // RFC 4918 11.4
|
||||
425: 'Too Early', // RFC 8470 5.2
|
||||
426: 'Upgrade Required', // RFC 2817 and RFC 7231 6.5.15
|
||||
428: 'Precondition Required', // RFC 6585 3
|
||||
429: 'Too Many Requests', // RFC 6585 4
|
||||
431: 'Request Header Fields Too Large', // RFC 6585 5
|
||||
451: 'Unavailable For Legal Reasons', // RFC 7725 3
|
||||
500: 'Internal Server Error', // RFC 7231 6.6.1
|
||||
501: 'Not Implemented', // RFC 7231 6.6.2
|
||||
502: 'Bad Gateway', // RFC 7231 6.6.3
|
||||
503: 'Service Unavailable', // RFC 7231 6.6.4
|
||||
504: 'Gateway Timeout', // RFC 7231 6.6.5
|
||||
505: 'HTTP Version Not Supported', // RFC 7231 6.6.6
|
||||
506: 'Variant Also Negotiates', // RFC 2295 8.1
|
||||
507: 'Insufficient Storage', // RFC 4918 11.5
|
||||
508: 'Loop Detected', // RFC 5842 7.2
|
||||
509: 'Bandwidth Limit Exceeded',
|
||||
510: 'Not Extended', // RFC 2774 7
|
||||
511: 'Network Authentication Required', // RFC 6585 6
|
||||
};
|
||||
|
||||
export function validateMsecs(numberlike: any, field: string) {
|
||||
if (typeof numberlike !== "number" || numberlike < 0) {
|
||||
throw $ERR_INVALID_ARG_TYPE(field, "number", numberlike);
|
||||
}
|
||||
|
||||
return numberlike;
|
||||
}
|
||||
13
src/js/internal/tls.ts
Normal file
13
src/js/internal/tls.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { isArrayBuffer, isTypedArray } from "node:util/types";
|
||||
|
||||
export function isValidTLSArray(obj) {
|
||||
if (typeof obj === "string" || isTypedArray(obj) || isArrayBuffer(obj) || $inheritsBlob(obj)) return true;
|
||||
if (Array.isArray(obj)) {
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
const item = obj[i];
|
||||
if (typeof item !== "string" && !isTypedArray(item) && !isArrayBuffer(item) && !$inheritsBlob(item)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
|
||||
* per the rules defined in RFC 7230
|
||||
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
|
||||
*/
|
||||
function checkIsHttpToken(val) {
|
||||
export function checkIsHttpToken(val) {
|
||||
return RegExpPrototypeExec.$call(tokenRegExp, val) !== null;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ function validateLinkHeaderFormat(value, name) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateLinkHeaderValue(hints) {
|
||||
export function validateLinkHeaderValue(hints) {
|
||||
if (typeof hints === "string") {
|
||||
validateLinkHeaderFormat(hints, "hints");
|
||||
return hints;
|
||||
@@ -68,45 +68,43 @@ function validateLinkHeaderValue(hints) {
|
||||
}
|
||||
hideFromStack(validateLinkHeaderValue);
|
||||
|
||||
export default {
|
||||
|
||||
/** (value, name) */
|
||||
validateObject: $newCppFunction("NodeValidator.cpp", "jsFunction_validateObject", 2),
|
||||
validateLinkHeaderValue: validateLinkHeaderValue,
|
||||
checkIsHttpToken: checkIsHttpToken,
|
||||
export const validateObject = $newCppFunction("NodeValidator.cpp", "jsFunction_validateObject", 2);
|
||||
/** `(value, name, min, max)` */
|
||||
validateInteger: $newCppFunction("NodeValidator.cpp", "jsFunction_validateInteger", 0),
|
||||
export const validateInteger = $newCppFunction("NodeValidator.cpp", "jsFunction_validateInteger", 0);
|
||||
/** `(value, name, min, max)` */
|
||||
validateNumber: $newCppFunction("NodeValidator.cpp", "jsFunction_validateNumber", 0),
|
||||
export const validateNumber = $newCppFunction("NodeValidator.cpp", "jsFunction_validateNumber", 0);
|
||||
/** `(value, name)` */
|
||||
validateString: $newCppFunction("NodeValidator.cpp", "jsFunction_validateString", 0),
|
||||
export const validateString = $newCppFunction("NodeValidator.cpp", "jsFunction_validateString", 0);
|
||||
/** `(number, name)` */
|
||||
validateFiniteNumber: $newCppFunction("NodeValidator.cpp", "jsFunction_validateFiniteNumber", 0),
|
||||
export const validateFiniteNumber = $newCppFunction("NodeValidator.cpp", "jsFunction_validateFiniteNumber", 0);
|
||||
/** `(number, name, lower, upper, def)` */
|
||||
checkRangesOrGetDefault: $newCppFunction("NodeValidator.cpp", "jsFunction_checkRangesOrGetDefault", 0),
|
||||
export const checkRangesOrGetDefault = $newCppFunction("NodeValidator.cpp", "jsFunction_checkRangesOrGetDefault", 0);
|
||||
/** `(value, name)` */
|
||||
validateFunction: $newCppFunction("NodeValidator.cpp", "jsFunction_validateFunction", 0),
|
||||
export const validateFunction = $newCppFunction("NodeValidator.cpp", "jsFunction_validateFunction", 0);
|
||||
/** `(value, name)` */
|
||||
validateBoolean: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBoolean", 0),
|
||||
export const validateBoolean = $newCppFunction("NodeValidator.cpp", "jsFunction_validateBoolean", 0);
|
||||
/** `(port, name = 'Port', allowZero = true)` */
|
||||
validatePort: $newCppFunction("NodeValidator.cpp", "jsFunction_validatePort", 0),
|
||||
export const validatePort = $newCppFunction("NodeValidator.cpp", "jsFunction_validatePort", 0);
|
||||
/** `(signal, name)` */
|
||||
validateAbortSignal: $newCppFunction("NodeValidator.cpp", "jsFunction_validateAbortSignal", 0),
|
||||
export const validateAbortSignal = $newCppFunction("NodeValidator.cpp", "jsFunction_validateAbortSignal", 0);
|
||||
/** `(value, name, minLength = 0)` */
|
||||
validateArray: $newCppFunction("NodeValidator.cpp", "jsFunction_validateArray", 0),
|
||||
export const validateArray = $newCppFunction("NodeValidator.cpp", "jsFunction_validateArray", 0);
|
||||
/** `(value, name, min = -2147483648, max = 2147483647)` */
|
||||
validateInt32: $newCppFunction("NodeValidator.cpp", "jsFunction_validateInt32", 0),
|
||||
export const validateInt32 = $newCppFunction("NodeValidator.cpp", "jsFunction_validateInt32", 0);
|
||||
/** `(value, name, positive = false)` */
|
||||
validateUint32: $newCppFunction("NodeValidator.cpp", "jsFunction_validateUint32", 0),
|
||||
export const validateUint32 = $newCppFunction("NodeValidator.cpp", "jsFunction_validateUint32", 0);
|
||||
/** `(signal, name = 'signal')` */
|
||||
validateSignalName: $newCppFunction("NodeValidator.cpp", "jsFunction_validateSignalName", 0),
|
||||
export const validateSignalName = $newCppFunction("NodeValidator.cpp", "jsFunction_validateSignalName", 0);
|
||||
/** `(data, encoding)` */
|
||||
validateEncoding: $newCppFunction("NodeValidator.cpp", "jsFunction_validateEncoding", 0),
|
||||
export const validateEncoding = $newCppFunction("NodeValidator.cpp", "jsFunction_validateEncoding", 0);
|
||||
/** `(value, name)` */
|
||||
validatePlainFunction: $newCppFunction("NodeValidator.cpp", "jsFunction_validatePlainFunction", 0),
|
||||
export const validatePlainFunction = $newCppFunction("NodeValidator.cpp", "jsFunction_validatePlainFunction", 0);
|
||||
/** `(value, name)` */
|
||||
validateUndefined: $newCppFunction("NodeValidator.cpp", "jsFunction_validateUndefined", 0),
|
||||
export const validateUndefined = $newCppFunction("NodeValidator.cpp", "jsFunction_validateUndefined", 0);
|
||||
/** `(buffer, name = 'buffer')` */
|
||||
validateBuffer: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBuffer", 0),
|
||||
export const validateBuffer = $newCppFunction("NodeValidator.cpp", "jsFunction_validateBuffer", 0);
|
||||
/** `(value, name, oneOf)` */
|
||||
validateOneOf: $newCppFunction("NodeValidator.cpp", "jsFunction_validateOneOf", 0),
|
||||
};
|
||||
export const validateOneOf = $newCppFunction("NodeValidator.cpp", "jsFunction_validateOneOf", 0);
|
||||
|
||||
|
||||
177
src/js/node/_http_agent.ts
Normal file
177
src/js/node/_http_agent.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { kFakeSocket, FakeSocket } from "internal/http/share";
|
||||
import EventEmitter from "node:events";
|
||||
// const net = require("node:net");
|
||||
const { validateNumber, validateOneOf } = require("internal/validators");
|
||||
// const { AsyncLocalStorage } = require("node:async_hooks");
|
||||
|
||||
const kEmptyObject = Object.freeze(Object.create(null));
|
||||
// const kOnKeylog = Symbol('onkeylog');
|
||||
// const kRequestOptions = Symbol('requestOptions');
|
||||
// const kRequestAsyncSnapshot = Symbol('requestAsyncResource');
|
||||
|
||||
// const HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER = 1000;
|
||||
const NODE_HTTP_WARNING =
|
||||
"WARN: Agent is mostly unused in Bun's implementation of http. If you see strange behavior, this is probably the cause.";
|
||||
|
||||
function Agent(options = kEmptyObject): void {
|
||||
if (!(this instanceof Agent)) return new Agent(options);
|
||||
EventEmitter.$apply(this, []);
|
||||
|
||||
this.defaultPort = 80;
|
||||
this.protocol = 'http:';
|
||||
|
||||
this.options = { __proto__: null, ...options };
|
||||
|
||||
if (this.options.noDelay === undefined)
|
||||
this.options.noDelay = true;
|
||||
|
||||
// Don't confuse net and make it think that we're connecting to a pipe
|
||||
this.options.path = null;
|
||||
this.requests = { __proto__: null };
|
||||
this.sockets = { __proto__: null };
|
||||
this.freeSockets = { __proto__: null };
|
||||
this.keepAliveMsecs = this.options.keepAliveMsecs || 1000;
|
||||
this.keepAlive = this.options.keepAlive || false;
|
||||
this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets;
|
||||
this.maxFreeSockets = this.options.maxFreeSockets || 256;
|
||||
this.scheduling = this.options.scheduling || 'lifo';
|
||||
this.maxTotalSockets = this.options.maxTotalSockets;
|
||||
this.totalSocketCount = 0;
|
||||
|
||||
validateOneOf(this.scheduling, 'scheduling', ['fifo', 'lifo']);
|
||||
|
||||
if (this.maxTotalSockets !== undefined) {
|
||||
validateNumber(this.maxTotalSockets, 'maxTotalSockets', 1);
|
||||
} else {
|
||||
this.maxTotalSockets = Infinity;
|
||||
}
|
||||
|
||||
// this.on('free', (socket, options) => {
|
||||
// const name = this.getName(options);
|
||||
// $debug('agent.on(free)', name);
|
||||
|
||||
// // TODO(ronag): socket.destroy(err) might have been called
|
||||
// // before coming here and have an 'error' scheduled. In the
|
||||
// // case of socket.destroy() below this 'error' has no handler
|
||||
// // and could cause unhandled exception.
|
||||
|
||||
// if (!socket.writable) {
|
||||
// socket.destroy();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const requests = this.requests[name];
|
||||
// if (requests?.length) {
|
||||
// const req = requests.shift();
|
||||
// const reqAsyncRes = req[kRequestAsyncSnapshot];
|
||||
// if (reqAsyncRes) {
|
||||
// // Run request within the original async context.
|
||||
// reqAsyncRes(() => {
|
||||
// asyncResetHandle(socket);
|
||||
// setRequestSocket(this, req, socket);
|
||||
// });
|
||||
// req[kRequestAsyncSnapshot] = null;
|
||||
// } else {
|
||||
// setRequestSocket(this, req, socket);
|
||||
// }
|
||||
// if (requests.length === 0) {
|
||||
// delete this.requests[name];
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // If there are no pending requests, then put it in
|
||||
// // the freeSockets pool, but only if we're allowed to do so.
|
||||
// const req = socket._httpMessage;
|
||||
// if (!req || !req.shouldKeepAlive || !this.keepAlive) {
|
||||
// socket.destroy();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const freeSockets = this.freeSockets[name] || [];
|
||||
// const freeLen = freeSockets.length;
|
||||
// let count = freeLen;
|
||||
// if (this.sockets[name])
|
||||
// count += this.sockets[name].length;
|
||||
|
||||
// if (this.totalSocketCount > this.maxTotalSockets ||
|
||||
// count > this.maxSockets ||
|
||||
// freeLen >= this.maxFreeSockets ||
|
||||
// !this.keepSocketAlive(socket)) {
|
||||
// socket.destroy();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.freeSockets[name] = freeSockets;
|
||||
// socket[async_id_symbol] = -1;
|
||||
// socket._httpMessage = null;
|
||||
// this.removeSocket(socket, options);
|
||||
|
||||
// socket.once('error', freeSocketErrorListener);
|
||||
// freeSockets.push(socket);
|
||||
// });
|
||||
|
||||
// Don't emit keylog events unless there is a listener for them.
|
||||
// this.on('newListener', maybeEnableKeylog);
|
||||
}
|
||||
$toClass(Agent, "Agent", EventEmitter);
|
||||
|
||||
var globalAgent;
|
||||
Object.defineProperty(Agent, "globalAgent", {
|
||||
get: function () {
|
||||
return globalAgent;
|
||||
},
|
||||
});
|
||||
|
||||
Agent.defaultMaxSockets = Infinity;
|
||||
|
||||
// Agent.prototype.createConnection = net.createConnection;
|
||||
Agent.prototype.createConnection = function () {
|
||||
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.createConnection is a no-op, returns fake socket");
|
||||
return (this[kFakeSocket] ??= new FakeSocket());
|
||||
};
|
||||
|
||||
// Get the key for a given set of request options
|
||||
Agent.prototype.getName = function (options = kEmptyObject) {
|
||||
let name = `http:${options.host || "localhost"}:`;
|
||||
if (options.port) name += options.port;
|
||||
name += ":";
|
||||
if (options.localAddress) name += options.localAddress;
|
||||
// Pacify parallel/test-http-agent-getname by only appending
|
||||
// the ':' when options.family is set.
|
||||
if (options.family === 4 || options.family === 6) name += `:${options.family}`;
|
||||
if (options.socketPath) name += `:${options.socketPath}`;
|
||||
return name;
|
||||
};
|
||||
|
||||
Agent.prototype.addRequest = function () {
|
||||
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.addRequest is a no-op");
|
||||
};
|
||||
|
||||
Agent.prototype.createSocket = function (req, options, cb) {
|
||||
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.createSocket returns fake socket");
|
||||
cb(null, (this[kFakeSocket] ??= new FakeSocket()));
|
||||
};
|
||||
|
||||
Agent.prototype.removeSocket = function () {
|
||||
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.removeSocket is a no-op");
|
||||
};
|
||||
|
||||
Agent.prototype.keepSocketAlive = function () {
|
||||
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.keepSocketAlive is a no-op");
|
||||
return true;
|
||||
};
|
||||
|
||||
Agent.prototype.reuseSocket = function () {
|
||||
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.reuseSocket is a no-op");
|
||||
};
|
||||
|
||||
Agent.prototype.destroy = function () {
|
||||
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.destroy is a no-op");
|
||||
};
|
||||
|
||||
globalAgent = new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 });
|
||||
export default {
|
||||
Agent,
|
||||
globalAgent,
|
||||
}
|
||||
938
src/js/node/_http_client.ts
Normal file
938
src/js/node/_http_client.ts
Normal file
@@ -0,0 +1,938 @@
|
||||
import http_agent from "./_http_agent";
|
||||
import { IncomingMessage } from "./_http_incoming";
|
||||
import { OutgoingMessage } from "./_http_outgoing";
|
||||
import { abortedSymbol, kAbortController, kAgent, kBodyChunks, kFetchRequest, kClearTimeout, kCloseCallback, kHost, kMaxHeadersCount, kMaxHeaderSize, kMethod, kParser, kPath, kPort, kProtocol, kRes, kReusedSocket, kSignal, kSocketPath, kTimeoutTimer, kTls, kUpgradeOrConnect, kUseDefaultPort, validateMsecs, swapIsNextIncomingMessageHTTPS, NodeHTTPIncomingRequestType, typeSymbol, reqSymbol, kJoinDuplicateHeaders, ClientRequestEmitState, kEmitState, kEmptyObject, kOptions, ConnResetException } from "internal/http/share";
|
||||
import { isIP, isIPv6 } from "node:net";
|
||||
import { checkIsHttpToken, validateFunction } from "internal/validators";
|
||||
import { isValidTLSArray } from "internal/tls";
|
||||
import { urlToHttpOptions } from "node:url";
|
||||
|
||||
const StringPrototypeToUpperCase = String.prototype.toUpperCase;
|
||||
const RegExpPrototypeExec = RegExp.prototype.exec;
|
||||
const ObjectAssign = Object.assign;
|
||||
|
||||
const NODE_HTTP_WARNING =
|
||||
"WARN: Agent is mostly unused in Bun's implementation of http. If you see strange behavior, this is probably the cause.";
|
||||
|
||||
const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
|
||||
|
||||
function ClientRequest(input, options, cb) {
|
||||
if (!(this instanceof ClientRequest)) {
|
||||
return new (ClientRequest as any)(input, options, cb);
|
||||
}
|
||||
|
||||
this.write = (chunk, encoding, callback) => {
|
||||
if (this.destroyed) return false;
|
||||
if ($isCallable(chunk)) {
|
||||
callback = chunk;
|
||||
chunk = undefined;
|
||||
encoding = undefined;
|
||||
} else if ($isCallable(encoding)) {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
} else if (!$isCallable(callback)) {
|
||||
callback = undefined;
|
||||
}
|
||||
|
||||
return write_(chunk, encoding, callback);
|
||||
};
|
||||
|
||||
let writeCount = 0;
|
||||
let resolveNextChunk: ((end: boolean) => void) | undefined = end => {};
|
||||
|
||||
const pushChunk = chunk => {
|
||||
this[kBodyChunks].push(chunk);
|
||||
if (writeCount > 1) {
|
||||
startFetch();
|
||||
}
|
||||
resolveNextChunk?.(false);
|
||||
};
|
||||
|
||||
const write_ = (chunk, encoding, callback) => {
|
||||
const MAX_FAKE_BACKPRESSURE_SIZE = 1024 * 1024;
|
||||
const canSkipReEncodingData =
|
||||
// UTF-8 string:
|
||||
(typeof chunk === "string" && (encoding === "utf-8" || encoding === "utf8" || !encoding)) ||
|
||||
// Buffer
|
||||
($isTypedArrayView(chunk) && (!encoding || encoding === "buffer" || encoding === "utf-8"));
|
||||
let bodySize = 0;
|
||||
if (!canSkipReEncodingData) {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
bodySize = chunk.length;
|
||||
writeCount++;
|
||||
|
||||
if (!this[kBodyChunks]) {
|
||||
this[kBodyChunks] = [];
|
||||
pushChunk(chunk);
|
||||
|
||||
if (callback) callback();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Signal fake backpressure if the body size is > 1024 * 1024
|
||||
// So that code which loops forever until backpressure is signaled
|
||||
// will eventually exit.
|
||||
|
||||
for (let chunk of this[kBodyChunks]) {
|
||||
bodySize += chunk.length;
|
||||
if (bodySize >= MAX_FAKE_BACKPRESSURE_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
pushChunk(chunk);
|
||||
|
||||
if (callback) callback();
|
||||
return bodySize < MAX_FAKE_BACKPRESSURE_SIZE;
|
||||
};
|
||||
|
||||
const oldEnd = this.end;
|
||||
|
||||
this.end = function (chunk, encoding, callback) {
|
||||
oldEnd?.$call(this, chunk, encoding, callback);
|
||||
|
||||
if ($isCallable(chunk)) {
|
||||
callback = chunk;
|
||||
chunk = undefined;
|
||||
encoding = undefined;
|
||||
} else if ($isCallable(encoding)) {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
} else if (!$isCallable(callback)) {
|
||||
callback = undefined;
|
||||
}
|
||||
|
||||
if (chunk) {
|
||||
if (this.finished) {
|
||||
emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_WRITE_AFTER_END(), callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
write_(chunk, encoding, null);
|
||||
} else if (this.finished) {
|
||||
if (callback) {
|
||||
if (!this.writableFinished) {
|
||||
this.on("finish", callback);
|
||||
} else {
|
||||
callback($ERR_STREAM_ALREADY_FINISHED("end"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
this.once("finish", callback);
|
||||
}
|
||||
|
||||
if (!this.finished) {
|
||||
send();
|
||||
resolveNextChunk?.(true);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
this.destroy = function (err?: Error) {
|
||||
if (this.destroyed) return this;
|
||||
this.destroyed = true;
|
||||
|
||||
const res = this.res;
|
||||
|
||||
// If we're aborting, we don't care about any more response data.
|
||||
if (res) {
|
||||
res._dump();
|
||||
}
|
||||
|
||||
this.finished = true;
|
||||
|
||||
// If request is destroyed we abort the current response
|
||||
this[kAbortController]?.abort?.();
|
||||
this.socket.destroy(err);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
this._ensureTls = () => {
|
||||
if (this[kTls] === null) this[kTls] = {};
|
||||
return this[kTls];
|
||||
};
|
||||
|
||||
const socketCloseListener = () => {
|
||||
this.destroyed = true;
|
||||
|
||||
const res = this.res;
|
||||
if (res) {
|
||||
// Socket closed before we emitted 'end' below.
|
||||
if (!res.complete) {
|
||||
res.destroy(new ConnResetException("aborted"));
|
||||
}
|
||||
if (!this._closed) {
|
||||
this._closed = true;
|
||||
callCloseCallback(this);
|
||||
this.emit("close");
|
||||
}
|
||||
if (!res.aborted && res.readable) {
|
||||
res.push(null);
|
||||
}
|
||||
} else if (!this._closed) {
|
||||
this._closed = true;
|
||||
callCloseCallback(this);
|
||||
this.emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
const onAbort = (err?: Error) => {
|
||||
this[kClearTimeout]?.();
|
||||
socketCloseListener();
|
||||
if (!this[abortedSymbol]) {
|
||||
process.nextTick(emitAbortNextTick, this);
|
||||
this[abortedSymbol] = true;
|
||||
}
|
||||
};
|
||||
|
||||
let fetching = false;
|
||||
|
||||
const startFetch = (customBody?) => {
|
||||
if (fetching) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fetching = true;
|
||||
|
||||
const method = this[kMethod];
|
||||
|
||||
let keepalive = true;
|
||||
const agentKeepalive = this[kAgent]?.keepalive;
|
||||
if (agentKeepalive !== undefined) {
|
||||
keepalive = agentKeepalive;
|
||||
}
|
||||
|
||||
const protocol = this[kProtocol];
|
||||
const path = this[kPath];
|
||||
let host = this[kHost];
|
||||
|
||||
const getURL = host => {
|
||||
if (isIPv6(host)) {
|
||||
host = `[${host}]`;
|
||||
}
|
||||
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return [path`${protocol}//${host}${this[kUseDefaultPort] ? "" : ":" + this[kPort]}`];
|
||||
} else {
|
||||
let proxy: string | undefined;
|
||||
const url = `${protocol}//${host}${this[kUseDefaultPort] ? "" : ":" + this[kPort]}${path}`;
|
||||
// support agent proxy url/string for http/https
|
||||
try {
|
||||
// getters can throw
|
||||
const agentProxy = this[kAgent]?.proxy;
|
||||
// this should work for URL like objects and strings
|
||||
proxy = agentProxy?.href || agentProxy;
|
||||
} catch {}
|
||||
return [url, proxy];
|
||||
}
|
||||
};
|
||||
|
||||
const go = (url, proxy, softFail = false) => {
|
||||
const tls =
|
||||
protocol === "https:" && this[kTls] ? { ...this[kTls], serverName: this[kTls].servername } : undefined;
|
||||
|
||||
const fetchOptions: any = {
|
||||
method,
|
||||
headers: this.getHeaders(),
|
||||
redirect: "manual",
|
||||
signal: this[kAbortController]?.signal,
|
||||
// Timeouts are handled via this.setTimeout.
|
||||
timeout: false,
|
||||
// Disable auto gzip/deflate
|
||||
decompress: false,
|
||||
keepalive,
|
||||
};
|
||||
let keepOpen = false;
|
||||
// no body and not finished
|
||||
const isDuplex = customBody === undefined && !this.finished;
|
||||
|
||||
if (isDuplex) {
|
||||
fetchOptions.duplex = "half";
|
||||
keepOpen = true;
|
||||
}
|
||||
|
||||
if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
|
||||
const self = this;
|
||||
if (customBody !== undefined) {
|
||||
fetchOptions.body = customBody;
|
||||
} else if (isDuplex) {
|
||||
fetchOptions.body = async function* () {
|
||||
while (self[kBodyChunks]?.length > 0) {
|
||||
yield self[kBodyChunks].shift();
|
||||
}
|
||||
|
||||
if (self[kBodyChunks]?.length === 0) {
|
||||
self.emit("drain");
|
||||
}
|
||||
|
||||
while (!self.finished) {
|
||||
yield await new Promise(resolve => {
|
||||
resolveNextChunk = end => {
|
||||
resolveNextChunk = undefined;
|
||||
if (end) {
|
||||
resolve(undefined);
|
||||
} else {
|
||||
resolve(self[kBodyChunks].shift());
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (self[kBodyChunks]?.length === 0) {
|
||||
self.emit("drain");
|
||||
}
|
||||
}
|
||||
|
||||
handleResponse?.();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (tls) {
|
||||
fetchOptions.tls = tls;
|
||||
}
|
||||
|
||||
if (!!$debug) {
|
||||
fetchOptions.verbose = true;
|
||||
}
|
||||
|
||||
if (proxy) {
|
||||
fetchOptions.proxy = proxy;
|
||||
}
|
||||
|
||||
const socketPath = this[kSocketPath];
|
||||
|
||||
if (socketPath) {
|
||||
fetchOptions.unix = socketPath;
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
this[kFetchRequest] = fetch(url, fetchOptions).then(response => {
|
||||
if (this.aborted) {
|
||||
maybeEmitClose();
|
||||
return;
|
||||
}
|
||||
|
||||
handleResponse = () => {
|
||||
this[kFetchRequest] = null;
|
||||
this[kClearTimeout]();
|
||||
handleResponse = undefined;
|
||||
const prevIsHTTPS = swapIsNextIncomingMessageHTTPS(response.url.startsWith("https:"));
|
||||
var res = (this.res = new IncomingMessage(response, {
|
||||
[typeSymbol]: NodeHTTPIncomingRequestType.FetchResponse,
|
||||
[reqSymbol]: this,
|
||||
}));
|
||||
swapIsNextIncomingMessageHTTPS(prevIsHTTPS);
|
||||
res.req = this;
|
||||
process.nextTick(
|
||||
(self, res) => {
|
||||
// If the user did not listen for the 'response' event, then they
|
||||
// can't possibly read the data, so we ._dump() it into the void
|
||||
// so that the socket doesn't hang there in a paused state.
|
||||
if (self.aborted || !self.emit("response", res)) {
|
||||
res._dump();
|
||||
}
|
||||
},
|
||||
this,
|
||||
res,
|
||||
);
|
||||
maybeEmitClose();
|
||||
if (res.statusCode === 304) {
|
||||
res.complete = true;
|
||||
maybeEmitClose();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (!keepOpen) {
|
||||
handleResponse();
|
||||
}
|
||||
|
||||
onEnd();
|
||||
});
|
||||
|
||||
if (!softFail) {
|
||||
// Don't emit an error if we're iterating over multiple possible addresses and we haven't reached the end yet.
|
||||
// This is for the happy eyeballs implementation.
|
||||
this[kFetchRequest]
|
||||
.catch(err => {
|
||||
// Node treats AbortError separately.
|
||||
// The "abort" listener on the abort controller should have called this
|
||||
if (isAbortError(err)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!$debug) globalThis.globalReportError(err);
|
||||
|
||||
this.emit("error", err);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!keepOpen) {
|
||||
fetching = false;
|
||||
this[kFetchRequest] = null;
|
||||
this[kClearTimeout]();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this[kFetchRequest];
|
||||
};
|
||||
|
||||
if (isIP(host) || !options.lookup) {
|
||||
// Don't need to bother with lookup if it's already an IP address or no lookup function is provided.
|
||||
const [url, proxy] = getURL(host);
|
||||
go(url, proxy, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
options.lookup(host, { all: true }, (err, results) => {
|
||||
if (err) {
|
||||
if (!!$debug) globalThis.globalReportError(err);
|
||||
this.emit("error", err);
|
||||
return;
|
||||
}
|
||||
|
||||
let candidates = results.sort((a, b) => b.family - a.family); // prefer IPv6
|
||||
|
||||
const fail = (message, name, code, syscall) => {
|
||||
const error = new Error(message);
|
||||
error.name = name;
|
||||
error.code = code;
|
||||
error.syscall = syscall;
|
||||
if (!!$debug) globalThis.globalReportError(error);
|
||||
this.emit("error", error);
|
||||
};
|
||||
|
||||
if (candidates.length === 0) {
|
||||
fail("No records found", "DNSException", "ENOTFOUND", "getaddrinfo");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hasHeader("Host")) {
|
||||
this.setHeader("Host", `${host}:${port}`);
|
||||
}
|
||||
|
||||
// We want to try all possible addresses, beginning with the IPv6 ones, until one succeeds.
|
||||
// All addresses except for the last are allowed to "soft fail" -- instead of reporting
|
||||
// an error to the user, we'll just skip to the next address.
|
||||
// The last address is required to work, and if it fails we'll throw an error.
|
||||
|
||||
const iterate = () => {
|
||||
if (candidates.length === 0) {
|
||||
// If we get to this point, it means that none of the addresses could be connected to.
|
||||
fail(`connect ECONNREFUSED ${host}:${port}`, "Error", "ECONNREFUSED", "connect");
|
||||
return;
|
||||
}
|
||||
|
||||
const [url, proxy] = getURL(candidates.shift().address);
|
||||
go(url, proxy, candidates.length > 0).catch(iterate);
|
||||
};
|
||||
|
||||
iterate();
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
let onEnd = () => {};
|
||||
let handleResponse: (() => void) | undefined = () => {};
|
||||
|
||||
const send = () => {
|
||||
this.finished = true;
|
||||
const controller = new AbortController();
|
||||
this[kAbortController] = controller;
|
||||
controller.signal.addEventListener("abort", onAbort, { once: true });
|
||||
|
||||
var body = this[kBodyChunks] && this[kBodyChunks].length > 1 ? new Blob(this[kBodyChunks]) : this[kBodyChunks]?.[0];
|
||||
|
||||
try {
|
||||
startFetch(body);
|
||||
onEnd = () => {
|
||||
handleResponse?.();
|
||||
};
|
||||
} catch (err) {
|
||||
if (!!$debug) globalThis.globalReportError(err);
|
||||
this.emit("error", err);
|
||||
} finally {
|
||||
process.nextTick(maybeEmitFinish.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
// --- For faking the events in the right order ---
|
||||
const maybeEmitSocket = () => {
|
||||
if (!(this[kEmitState] & (1 << ClientRequestEmitState.socket))) {
|
||||
this[kEmitState] |= 1 << ClientRequestEmitState.socket;
|
||||
this.emit("socket", this.socket);
|
||||
}
|
||||
};
|
||||
|
||||
const maybeEmitPrefinish = () => {
|
||||
maybeEmitSocket();
|
||||
|
||||
if (!(this[kEmitState] & (1 << ClientRequestEmitState.prefinish))) {
|
||||
this[kEmitState] |= 1 << ClientRequestEmitState.prefinish;
|
||||
this.emit("prefinish");
|
||||
}
|
||||
};
|
||||
|
||||
const maybeEmitFinish = () => {
|
||||
maybeEmitPrefinish();
|
||||
|
||||
if (!(this[kEmitState] & (1 << ClientRequestEmitState.finish))) {
|
||||
this[kEmitState] |= 1 << ClientRequestEmitState.finish;
|
||||
this.emit("finish");
|
||||
}
|
||||
};
|
||||
|
||||
const maybeEmitClose = () => {
|
||||
maybeEmitPrefinish();
|
||||
|
||||
if (!this._closed) {
|
||||
process.nextTick(emitCloseNTAndComplete, this);
|
||||
}
|
||||
};
|
||||
|
||||
this.abort = () => {
|
||||
if (this.aborted) return;
|
||||
this[abortedSymbol] = true;
|
||||
process.nextTick(emitAbortNextTick, this);
|
||||
this[kAbortController]?.abort?.();
|
||||
this.destroy();
|
||||
};
|
||||
|
||||
if (typeof input === "string") {
|
||||
const urlStr = input;
|
||||
try {
|
||||
var urlObject = new URL(urlStr);
|
||||
} catch (e) {
|
||||
throw $ERR_INVALID_URL(`Invalid URL: ${urlStr}`);
|
||||
}
|
||||
input = urlToHttpOptions(urlObject);
|
||||
} else if (input && typeof input === "object" && input instanceof URL) {
|
||||
// url.URL instance
|
||||
input = urlToHttpOptions(input);
|
||||
} else {
|
||||
cb = options;
|
||||
options = input;
|
||||
input = null;
|
||||
}
|
||||
|
||||
if (typeof options === "function") {
|
||||
cb = options;
|
||||
options = input || kEmptyObject;
|
||||
} else {
|
||||
options = ObjectAssign(input || {}, options);
|
||||
}
|
||||
|
||||
this[kTls] = null;
|
||||
this[kAbortController] = null;
|
||||
|
||||
let agent = options.agent;
|
||||
const defaultAgent = options._defaultAgent || http_agent.globalAgent;
|
||||
if (agent === false) {
|
||||
agent = new defaultAgent.constructor();
|
||||
} else if (agent == null) {
|
||||
agent = defaultAgent;
|
||||
} else if (typeof agent.addRequest !== "function") {
|
||||
throw $ERR_INVALID_ARG_TYPE("options.agent", "Agent-like Object, undefined, or false", agent);
|
||||
}
|
||||
this[kAgent] = agent;
|
||||
this.destroyed = false;
|
||||
|
||||
const protocol = options.protocol || defaultAgent.protocol;
|
||||
let expectedProtocol = defaultAgent.protocol;
|
||||
if (this.agent.protocol) {
|
||||
expectedProtocol = this.agent.protocol;
|
||||
}
|
||||
if (protocol !== expectedProtocol) {
|
||||
throw $ERR_INVALID_PROTOCOL(protocol, expectedProtocol);
|
||||
}
|
||||
this[kProtocol] = protocol;
|
||||
|
||||
if (options.path) {
|
||||
const path = String(options.path);
|
||||
if (RegExpPrototypeExec.$call(INVALID_PATH_REGEX, path) !== null) {
|
||||
throw $ERR_UNESCAPED_CHARACTERS("Request path");
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPort = options.defaultPort || this[kAgent].defaultPort;
|
||||
const port = (this[kPort] = options.port || defaultPort || 80);
|
||||
this[kUseDefaultPort] = this[kPort] === defaultPort;
|
||||
const host =
|
||||
(this[kHost] =
|
||||
options.host =
|
||||
validateHost(options.hostname, "hostname") || validateHost(options.host, "host") || "localhost");
|
||||
|
||||
const setHost = options.setHost === undefined || Boolean(options.setHost);
|
||||
|
||||
this[kSocketPath] = options.socketPath;
|
||||
|
||||
const signal = options.signal;
|
||||
if (signal) {
|
||||
//We still want to control abort function and timeout so signal call our AbortController
|
||||
signal.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
this[kAbortController]?.abort?.();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
this[kSignal] = signal;
|
||||
}
|
||||
let method = options.method;
|
||||
const methodIsString = typeof method === "string";
|
||||
if (method !== null && method !== undefined && !methodIsString) {
|
||||
throw $ERR_INVALID_ARG_TYPE("options.method", "string", method);
|
||||
}
|
||||
|
||||
if (methodIsString && method) {
|
||||
if (!checkIsHttpToken(method)) {
|
||||
throw $ERR_INVALID_HTTP_TOKEN("Method", method);
|
||||
}
|
||||
method = this[kMethod] = StringPrototypeToUpperCase.$call(method);
|
||||
} else {
|
||||
method = this[kMethod] = "GET";
|
||||
}
|
||||
|
||||
const _maxHeaderSize = options.maxHeaderSize;
|
||||
// TODO: Validators
|
||||
// if (maxHeaderSize !== undefined)
|
||||
// validateInteger(maxHeaderSize, "maxHeaderSize", 0);
|
||||
this[kMaxHeaderSize] = _maxHeaderSize;
|
||||
|
||||
// const insecureHTTPParser = options.insecureHTTPParser;
|
||||
// if (insecureHTTPParser !== undefined) {
|
||||
// validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
|
||||
// }
|
||||
|
||||
// this.insecureHTTPParser = insecureHTTPParser;
|
||||
var _joinDuplicateHeaders = options.joinDuplicateHeaders;
|
||||
if (_joinDuplicateHeaders !== undefined) {
|
||||
// TODO: Validators
|
||||
// validateBoolean(
|
||||
// options.joinDuplicateHeaders,
|
||||
// "options.joinDuplicateHeaders",
|
||||
// );
|
||||
}
|
||||
|
||||
this[kJoinDuplicateHeaders] = _joinDuplicateHeaders;
|
||||
if (options.pfx) {
|
||||
throw new Error("pfx is not supported");
|
||||
}
|
||||
|
||||
if (options.rejectUnauthorized !== undefined) this._ensureTls().rejectUnauthorized = options.rejectUnauthorized;
|
||||
else {
|
||||
let agentRejectUnauthorized = agent?.options?.rejectUnauthorized;
|
||||
if (agentRejectUnauthorized !== undefined) this._ensureTls().rejectUnauthorized = agentRejectUnauthorized;
|
||||
else {
|
||||
// popular https-proxy-agent uses connectOpts
|
||||
agentRejectUnauthorized = agent?.connectOpts?.rejectUnauthorized;
|
||||
if (agentRejectUnauthorized !== undefined) this._ensureTls().rejectUnauthorized = agentRejectUnauthorized;
|
||||
}
|
||||
}
|
||||
if (options.ca) {
|
||||
if (!isValidTLSArray(options.ca))
|
||||
throw new TypeError(
|
||||
"ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile",
|
||||
);
|
||||
this._ensureTls().ca = options.ca;
|
||||
}
|
||||
if (options.cert) {
|
||||
if (!isValidTLSArray(options.cert))
|
||||
throw new TypeError(
|
||||
"cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile",
|
||||
);
|
||||
this._ensureTls().cert = options.cert;
|
||||
}
|
||||
if (options.key) {
|
||||
if (!isValidTLSArray(options.key))
|
||||
throw new TypeError(
|
||||
"key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile",
|
||||
);
|
||||
this._ensureTls().key = options.key;
|
||||
}
|
||||
if (options.passphrase) {
|
||||
if (typeof options.passphrase !== "string") throw new TypeError("passphrase argument must be a string");
|
||||
this._ensureTls().passphrase = options.passphrase;
|
||||
}
|
||||
if (options.ciphers) {
|
||||
if (typeof options.ciphers !== "string") throw new TypeError("ciphers argument must be a string");
|
||||
this._ensureTls().ciphers = options.ciphers;
|
||||
}
|
||||
if (options.servername) {
|
||||
if (typeof options.servername !== "string") throw new TypeError("servername argument must be a string");
|
||||
this._ensureTls().servername = options.servername;
|
||||
}
|
||||
|
||||
if (options.secureOptions) {
|
||||
if (typeof options.secureOptions !== "number") throw new TypeError("secureOptions argument must be a string");
|
||||
this._ensureTls().secureOptions = options.secureOptions;
|
||||
}
|
||||
this[kPath] = options.path || "/";
|
||||
if (cb) {
|
||||
this.once("response", cb);
|
||||
}
|
||||
|
||||
$debug(`new ClientRequest: ${this[kMethod]} ${this[kProtocol]}//${this[kHost]}:${this[kPort]}${this[kPath]}`);
|
||||
|
||||
// if (
|
||||
// method === "GET" ||
|
||||
// method === "HEAD" ||
|
||||
// method === "DELETE" ||
|
||||
// method === "OPTIONS" ||
|
||||
// method === "TRACE" ||
|
||||
// method === "CONNECT"
|
||||
// ) {
|
||||
// this.useChunkedEncodingByDefault = false;
|
||||
// } else {
|
||||
// this.useChunkedEncodingByDefault = true;
|
||||
// }
|
||||
|
||||
this.finished = false;
|
||||
this[kRes] = null;
|
||||
this[kUpgradeOrConnect] = false;
|
||||
this[kParser] = null;
|
||||
this[kMaxHeadersCount] = null;
|
||||
this[kReusedSocket] = false;
|
||||
this[kHost] = host;
|
||||
this[kProtocol] = protocol;
|
||||
|
||||
const timeout = options.timeout;
|
||||
if (timeout !== undefined && timeout !== 0) {
|
||||
this.setTimeout(timeout, undefined);
|
||||
}
|
||||
|
||||
const { headers } = options;
|
||||
const headersArray = $isJSArray(headers);
|
||||
if (!headersArray) {
|
||||
if (headers) {
|
||||
for (let key in headers) {
|
||||
this.setHeader(key, 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 = StringPrototypeIndexOf.$call(hostHeader, ":");
|
||||
// if (
|
||||
// posColon !== -1 &&
|
||||
// StringPrototypeIncludes.$call(hostHeader, ":", posColon + 1) &&
|
||||
// StringPrototypeCharCodeAt.$call(hostHeader, 0) !== 91 /* '[' */
|
||||
// ) {
|
||||
// hostHeader = `[${hostHeader}]`;
|
||||
// }
|
||||
|
||||
// if (port && +port !== defaultPort) {
|
||||
// hostHeader += ":" + port;
|
||||
// }
|
||||
// this.setHeader("Host", hostHeader);
|
||||
// }
|
||||
|
||||
var auth = options.auth;
|
||||
if (auth && !this.getHeader("Authorization")) {
|
||||
this.setHeader("Authorization", "Basic " + Buffer.from(auth).toString("base64"));
|
||||
}
|
||||
|
||||
// if (this.getHeader("expect")) {
|
||||
// if (this._header) {
|
||||
// throw new ERR_HTTP_HEADERS_SENT("render");
|
||||
// }
|
||||
|
||||
// this._storeHeader(
|
||||
// this.method + " " + this.path + " HTTP/1.1\r\n",
|
||||
// this[kOutHeaders],
|
||||
// );
|
||||
// }
|
||||
// } else {
|
||||
// this._storeHeader(
|
||||
// this.method + " " + this.path + " HTTP/1.1\r\n",
|
||||
// options.headers,
|
||||
// );
|
||||
}
|
||||
|
||||
// this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
|
||||
|
||||
const { signal: _signal, ...optsWithoutSignal } = options;
|
||||
this[kOptions] = optsWithoutSignal;
|
||||
|
||||
this._httpMessage = this;
|
||||
|
||||
process.nextTick(emitContinueAndSocketNT, this);
|
||||
|
||||
this[kEmitState] = 0;
|
||||
|
||||
this.setSocketKeepAlive = (enable = true, initialDelay = 0) => {
|
||||
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: ClientRequest.setSocketKeepAlive is a no-op");
|
||||
};
|
||||
|
||||
this.setNoDelay = (noDelay = true) => {
|
||||
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: ClientRequest.setNoDelay is a no-op");
|
||||
};
|
||||
|
||||
this[kClearTimeout] = () => {
|
||||
const timeoutTimer = this[kTimeoutTimer];
|
||||
if (timeoutTimer) {
|
||||
clearTimeout(timeoutTimer);
|
||||
this[kTimeoutTimer] = undefined;
|
||||
this.removeAllListeners("timeout");
|
||||
}
|
||||
};
|
||||
|
||||
const onTimeout = () => {
|
||||
this[kTimeoutTimer] = undefined;
|
||||
this[kAbortController]?.abort?.();
|
||||
this.emit("timeout");
|
||||
};
|
||||
|
||||
this.setTimeout = (msecs, callback) => {
|
||||
if (this.destroyed) return this;
|
||||
|
||||
this.timeout = msecs = validateMsecs(msecs, "msecs");
|
||||
|
||||
// Attempt to clear an existing timer in both cases -
|
||||
// even if it will be rescheduled we don't want to leak an existing timer.
|
||||
clearTimeout(this[kTimeoutTimer]!);
|
||||
|
||||
if (msecs === 0) {
|
||||
if (callback !== undefined) {
|
||||
validateFunction(callback, "callback");
|
||||
this.removeListener("timeout", callback);
|
||||
}
|
||||
|
||||
this[kTimeoutTimer] = undefined;
|
||||
} else {
|
||||
this[kTimeoutTimer] = setTimeout(onTimeout, msecs).unref();
|
||||
|
||||
if (callback !== undefined) {
|
||||
validateFunction(callback, "callback");
|
||||
this.once("timeout", callback);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
const ClientRequestPrototype = {
|
||||
constructor: ClientRequest,
|
||||
__proto__: OutgoingMessage.prototype,
|
||||
|
||||
// TODO: add setters for all these
|
||||
|
||||
get path() {
|
||||
return this[kPath];
|
||||
},
|
||||
|
||||
get port() {
|
||||
return this[kPort];
|
||||
},
|
||||
|
||||
get method() {
|
||||
return this[kMethod];
|
||||
},
|
||||
|
||||
get host() {
|
||||
return this[kHost];
|
||||
},
|
||||
|
||||
get protocol() {
|
||||
return this[kProtocol];
|
||||
},
|
||||
|
||||
get agent() {
|
||||
return this[kAgent];
|
||||
},
|
||||
|
||||
set agent(value) {
|
||||
this[kAgent] = value;
|
||||
},
|
||||
|
||||
get aborted() {
|
||||
return this[abortedSymbol] || this[kSignal]?.aborted || !!this[kAbortController]?.signal?.aborted;
|
||||
},
|
||||
|
||||
set aborted(value) {
|
||||
this[abortedSymbol] = value;
|
||||
},
|
||||
|
||||
get writable() {
|
||||
return !this.finished;
|
||||
},
|
||||
};
|
||||
|
||||
ClientRequest.prototype = ClientRequestPrototype;
|
||||
$setPrototypeDirect.$call(ClientRequest, OutgoingMessage);
|
||||
|
||||
function emitErrorNextTickIfErrorListenerNT(self, err, cb) {
|
||||
process.nextTick(emitErrorNextTickIfErrorListener, self, err, cb);
|
||||
}
|
||||
|
||||
function emitErrorNextTickIfErrorListener(self, err, cb) {
|
||||
if ($isCallable(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(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function callCloseCallback(self) {
|
||||
if (self[kCloseCallback]) {
|
||||
self[kCloseCallback]();
|
||||
self[kCloseCallback] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function emitAbortNextTick(self) {
|
||||
self.emit("abort");
|
||||
}
|
||||
|
||||
function isAbortError(err) {
|
||||
return err?.name === "AbortError";
|
||||
}
|
||||
|
||||
function validateHost(host, name) {
|
||||
if (host !== null && host !== undefined && typeof host !== "string") {
|
||||
throw $ERR_INVALID_ARG_TYPE(`options.${name}`, ["string", "undefined", "null"], host);
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
function emitCloseNTAndComplete(self) {
|
||||
if (!self._closed) {
|
||||
self._closed = true;
|
||||
callCloseCallback(self);
|
||||
self.emit("close");
|
||||
}
|
||||
|
||||
self.complete = true;
|
||||
}
|
||||
|
||||
function emitContinueAndSocketNT(self) {
|
||||
if (self.destroyed) return;
|
||||
// Ref: https://github.com/nodejs/node/blob/f63e8b7fa7a4b5e041ddec67307609ec8837154f/lib/_http_client.js#L803-L839
|
||||
if (!(self[kEmitState] & (1 << ClientRequestEmitState.socket))) {
|
||||
self[kEmitState] |= 1 << ClientRequestEmitState.socket;
|
||||
self.emit("socket", self.socket);
|
||||
}
|
||||
|
||||
// Emit continue event for the client (internally we auto handle it)
|
||||
if (!self._closed && self.getHeader("expect") === "100-continue") {
|
||||
self.emit("continue");
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
ClientRequest
|
||||
};
|
||||
250
src/js/node/_http_common.ts
Normal file
250
src/js/node/_http_common.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
// IncomingMessage,
|
||||
readStart,
|
||||
readStop,
|
||||
} = require('node:_http_incoming');
|
||||
|
||||
const methods = [
|
||||
'DELETE', 'GET', 'HEAD',
|
||||
'POST', 'PUT', 'CONNECT',
|
||||
'OPTIONS', 'TRACE', 'COPY',
|
||||
'LOCK', 'MKCOL', 'MOVE',
|
||||
'PROPFIND', 'PROPPATCH', 'SEARCH',
|
||||
'UNLOCK', 'BIND', 'REBIND',
|
||||
'UNBIND', 'ACL', 'REPORT',
|
||||
'MKACTIVITY', 'CHECKOUT', 'MERGE',
|
||||
'M-SEARCH', 'NOTIFY', 'SUBSCRIBE',
|
||||
'UNSUBSCRIBE', 'PATCH', 'PURGE',
|
||||
'MKCALENDAR', 'LINK', 'UNLINK',
|
||||
'SOURCE', 'QUERY'
|
||||
];
|
||||
|
||||
const kIncomingMessage = Symbol('IncomingMessage');
|
||||
// const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0;
|
||||
// const kOnHeaders = HTTPParser.kOnHeaders | 0;
|
||||
// const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
|
||||
// const kOnBody = HTTPParser.kOnBody | 0;
|
||||
// const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
|
||||
// const kOnExecute = HTTPParser.kOnExecute | 0;
|
||||
// const kOnTimeout = HTTPParser.kOnTimeout | 0;
|
||||
|
||||
// const MAX_HEADER_PAIRS = 2000;
|
||||
|
||||
// Only called in the slow case where slow means
|
||||
// that the request headers were either fragmented
|
||||
// across multiple TCP packets or too large to be
|
||||
// processed in a single run. This method is also
|
||||
// called to process trailing HTTP headers.
|
||||
// function parserOnHeaders(headers, url) {
|
||||
// // Once we exceeded headers limit - stop collecting them
|
||||
// if (this.maxHeaderPairs <= 0 ||
|
||||
// this._headers.length < this.maxHeaderPairs) {
|
||||
// this._headers.push(...headers);
|
||||
// }
|
||||
// this._url += url;
|
||||
// }
|
||||
|
||||
// `headers` and `url` are set only if .onHeaders() has not been called for
|
||||
// this request.
|
||||
// `url` is not set for response parsers but that's not applicable here since
|
||||
// all our parsers are request parsers.
|
||||
// function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
|
||||
// url, statusCode, statusMessage, upgrade,
|
||||
// shouldKeepAlive) {
|
||||
// const parser = this;
|
||||
// const { socket } = parser;
|
||||
|
||||
// if (headers === undefined) {
|
||||
// headers = parser._headers;
|
||||
// parser._headers = [];
|
||||
// }
|
||||
|
||||
// if (url === undefined) {
|
||||
// url = parser._url;
|
||||
// parser._url = '';
|
||||
// }
|
||||
|
||||
// // Parser is also used by http client
|
||||
// const ParserIncomingMessage = (socket?.server?.[kIncomingMessage]) ||
|
||||
// IncomingMessage;
|
||||
|
||||
// const incoming = parser.incoming = new ParserIncomingMessage(socket);
|
||||
// incoming.httpVersionMajor = versionMajor;
|
||||
// incoming.httpVersionMinor = versionMinor;
|
||||
// incoming.httpVersion = `${versionMajor}.${versionMinor}`;
|
||||
// incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders ||
|
||||
// parser.joinDuplicateHeaders;
|
||||
// incoming.url = url;
|
||||
// incoming.upgrade = upgrade;
|
||||
|
||||
// let n = headers.length;
|
||||
|
||||
// // If parser.maxHeaderPairs <= 0 assume that there's no limit.
|
||||
// if (parser.maxHeaderPairs > 0)
|
||||
// n = $min(n, parser.maxHeaderPairs);
|
||||
|
||||
// incoming._addHeaderLines(headers, n);
|
||||
|
||||
// if (typeof method === 'number') {
|
||||
// // server only
|
||||
// incoming.method = allMethods[method];
|
||||
// } else {
|
||||
// // client only
|
||||
// incoming.statusCode = statusCode;
|
||||
// incoming.statusMessage = statusMessage;
|
||||
// }
|
||||
|
||||
// return parser.onIncoming(incoming, shouldKeepAlive);
|
||||
// }
|
||||
|
||||
function parserOnBody(b) {
|
||||
const stream = this.incoming;
|
||||
|
||||
// If the stream has already been removed, then drop it.
|
||||
if (stream === null)
|
||||
return;
|
||||
|
||||
// Pretend this was the result of a stream._read call.
|
||||
if (!stream._dumped) {
|
||||
const ret = stream.push(b);
|
||||
if (!ret)
|
||||
readStop(this.socket);
|
||||
}
|
||||
}
|
||||
|
||||
function parserOnMessageComplete() {
|
||||
const parser = this;
|
||||
const stream = parser.incoming;
|
||||
|
||||
if (stream !== null) {
|
||||
stream.complete = true;
|
||||
// Emit any trailing headers.
|
||||
const headers = parser._headers;
|
||||
if (headers.length) {
|
||||
stream._addHeaderLines(headers, headers.length);
|
||||
parser._headers = [];
|
||||
parser._url = '';
|
||||
}
|
||||
|
||||
// For emit end event
|
||||
stream.push(null);
|
||||
}
|
||||
|
||||
// Force to read the next incoming message
|
||||
readStart(parser.socket);
|
||||
}
|
||||
|
||||
|
||||
// const parsers = new FreeList('parsers', 1000, function parsersCb() {
|
||||
// const parser = new HTTPParser();
|
||||
|
||||
// cleanParser(parser);
|
||||
|
||||
// parser[kOnHeaders] = parserOnHeaders;
|
||||
// parser[kOnHeadersComplete] = parserOnHeadersComplete;
|
||||
// parser[kOnBody] = parserOnBody;
|
||||
// parser[kOnMessageComplete] = parserOnMessageComplete;
|
||||
|
||||
// return parser;
|
||||
// });
|
||||
|
||||
// function closeParserInstance(parser) { parser.close(); }
|
||||
|
||||
// Free the parser and also break any links that it
|
||||
// might have to any other things.
|
||||
// TODO: All parser data should be attached to a
|
||||
// single object, so that it can be easily cleaned
|
||||
// up by doing `parser.data = {}`, which should
|
||||
// be done in FreeList.free. `parsers.free(parser)`
|
||||
// should be all that is needed.
|
||||
// function freeParser(parser, req, socket) {
|
||||
// if (parser) {
|
||||
// if (parser._consumed)
|
||||
// parser.unconsume();
|
||||
// cleanParser(parser);
|
||||
// parser.remove();
|
||||
// if (parsers.free(parser) === false) {
|
||||
// // Make sure the parser's stack has unwound before deleting the
|
||||
// // corresponding C++ object through .close().
|
||||
// setImmediate(closeParserInstance, parser);
|
||||
// } else {
|
||||
// // Since the Parser destructor isn't going to run the destroy() callbacks
|
||||
// // it needs to be triggered manually.
|
||||
// parser.free();
|
||||
// }
|
||||
// }
|
||||
// if (req) {
|
||||
// req.parser = null;
|
||||
// }
|
||||
// if (socket) {
|
||||
// socket.parser = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
|
||||
/**
|
||||
* Verifies that the given val is a valid HTTP token
|
||||
* per the rules defined in RFC 7230
|
||||
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
|
||||
*/
|
||||
function checkIsHttpToken(val) {
|
||||
return tokenRegExp.test(val);
|
||||
}
|
||||
|
||||
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
|
||||
/**
|
||||
* True if val contains an invalid field-vchar
|
||||
* field-value = *( field-content / obs-fold )
|
||||
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
||||
* field-vchar = VCHAR / obs-text
|
||||
*/
|
||||
function checkInvalidHeaderChar(val) {
|
||||
return headerCharRegex.test(val);
|
||||
}
|
||||
|
||||
// function cleanParser(parser) {
|
||||
// parser._headers = [];
|
||||
// parser._url = '';
|
||||
// parser.socket = null;
|
||||
// parser.incoming = null;
|
||||
// parser.outgoing = null;
|
||||
// parser.maxHeaderPairs = MAX_HEADER_PAIRS;
|
||||
// parser[kOnMessageBegin] = null;
|
||||
// parser[kOnExecute] = null;
|
||||
// parser[kOnTimeout] = null;
|
||||
// parser._consumed = false;
|
||||
// parser.onIncoming = null;
|
||||
// parser.joinDuplicateHeaders = null;
|
||||
// }
|
||||
|
||||
function prepareError(err, parser, rawPacket) {
|
||||
err.rawPacket = rawPacket || parser.getCurrentBuffer();
|
||||
if (typeof err.reason === 'string')
|
||||
err.message = `Parse Error: ${err.reason}`;
|
||||
}
|
||||
|
||||
function isLenient() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
_checkInvalidHeaderChar: checkInvalidHeaderChar,
|
||||
_checkIsHttpToken: checkIsHttpToken,
|
||||
chunkExpression: /(?:^|\W)chunked(?:$|\W)/i,
|
||||
continueExpression: /(?:^|\W)100-continue(?:$|\W)/i,
|
||||
CRLF: '\r\n', // TODO: Deprecate this.
|
||||
freeParser: function() {
|
||||
throw new Error('TODO: _http_common.freeParser is not available in Bun');
|
||||
},
|
||||
methods,
|
||||
parsers: [],
|
||||
kIncomingMessage,
|
||||
HTTPParser: function() {
|
||||
throw new Error('TODO: _http_common.HTTPParser is not available in Bun');
|
||||
},
|
||||
isLenient,
|
||||
prepareError,
|
||||
};
|
||||
414
src/js/node/_http_incoming.ts
Normal file
414
src/js/node/_http_incoming.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
import { Readable, finished } from "node:stream";
|
||||
import { assignHeadersFast, eofInProgress, FakeSocket, headersTuple, kEmptyObject, kFakeSocket, kHandle, NodeHTTPBodyReadState, NodeHTTPIncomingRequestType, NodeHTTPResponseAbortEvent, setRequestTimeout, STATUS_CODES, statusCodeSymbol, statusMessageSymbol, swapIsNextIncomingMessageHTTPS, webRequestOrResponse } from "internal/http/share";
|
||||
|
||||
const kInternalRequest = Symbol("kInternalRequest");
|
||||
const typeSymbol = Symbol("type");
|
||||
const reqSymbol = Symbol("req");
|
||||
const bodyStreamSymbol = Symbol("bodyStream");
|
||||
const noBodySymbol = Symbol("noBody");
|
||||
const abortedSymbol = Symbol("aborted");
|
||||
|
||||
export function readStart(socket) {
|
||||
if (socket && !socket._paused && socket.readable)
|
||||
socket.resume();
|
||||
}
|
||||
|
||||
export function readStop(socket) {
|
||||
if (socket)
|
||||
socket.pause();
|
||||
}
|
||||
|
||||
function onIncomingMessagePauseNodeHTTPResponse(this: IncomingMessage) {
|
||||
const handle = this[kHandle];
|
||||
if (handle && !this.destroyed) {
|
||||
const paused = handle.pause();
|
||||
}
|
||||
}
|
||||
|
||||
function onIncomingMessageResumeNodeHTTPResponse(this: IncomingMessage) {
|
||||
const handle = this[kHandle];
|
||||
if (handle && !this.destroyed) {
|
||||
const resumed = handle.resume();
|
||||
if (resumed && resumed !== true) {
|
||||
const bodyReadState = handle.hasBody;
|
||||
if ((bodyReadState & NodeHTTPBodyReadState.done) !== 0) {
|
||||
emitEOFIncomingMessage(this);
|
||||
}
|
||||
this.push(resumed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emitEOFIncomingMessageOuter(self) {
|
||||
self.push(null);
|
||||
self.complete = true;
|
||||
}
|
||||
|
||||
function emitEOFIncomingMessage(self) {
|
||||
self[eofInProgress] = true;
|
||||
process.nextTick(emitEOFIncomingMessageOuter, self);
|
||||
}
|
||||
|
||||
export type IncomingMessage = any; // wehh
|
||||
export function IncomingMessage(req, defaultIncomingOpts): void {
|
||||
this[abortedSymbol] = false;
|
||||
this[eofInProgress] = false;
|
||||
this._consuming = false;
|
||||
this._dumped = false;
|
||||
this.complete = false;
|
||||
this._closed = false;
|
||||
|
||||
// (url, method, headers, rawHeaders, handle, hasBody)
|
||||
if (req === kHandle) {
|
||||
this[typeSymbol] = NodeHTTPIncomingRequestType.NodeHTTPResponse;
|
||||
this.url = arguments[1];
|
||||
this.method = arguments[2];
|
||||
this.headers = arguments[3];
|
||||
this.rawHeaders = arguments[4];
|
||||
this[kHandle] = arguments[5];
|
||||
this[noBodySymbol] = !arguments[6];
|
||||
this[kFakeSocket] = arguments[7];
|
||||
Readable.$call(this);
|
||||
|
||||
// If there's a body, pay attention to pause/resume events
|
||||
if (arguments[6]) {
|
||||
this.on("pause", onIncomingMessagePauseNodeHTTPResponse);
|
||||
this.on("resume", onIncomingMessageResumeNodeHTTPResponse);
|
||||
}
|
||||
} else {
|
||||
this[noBodySymbol] = false;
|
||||
Readable.$call(this);
|
||||
var { [typeSymbol]: type, [reqSymbol]: nodeReq } = defaultIncomingOpts || {};
|
||||
|
||||
this[webRequestOrResponse] = req;
|
||||
this[typeSymbol] = type;
|
||||
this[bodyStreamSymbol] = undefined;
|
||||
this[statusMessageSymbol] = (req as Response)?.statusText || null;
|
||||
this[statusCodeSymbol] = (req as Response)?.status || 200;
|
||||
|
||||
if (type === NodeHTTPIncomingRequestType.FetchRequest || type === NodeHTTPIncomingRequestType.FetchResponse) {
|
||||
if (!assignHeaders(this, req)) {
|
||||
this[kFakeSocket] = req;
|
||||
}
|
||||
} else {
|
||||
// Node defaults url and method to null.
|
||||
this.url = "";
|
||||
this.method = null;
|
||||
this.rawHeaders = [];
|
||||
}
|
||||
|
||||
this[noBodySymbol] =
|
||||
type === NodeHTTPIncomingRequestType.FetchRequest // TODO: Add logic for checking for body on response
|
||||
? requestHasNoBody(this.method, this)
|
||||
: false;
|
||||
|
||||
if (swapIsNextIncomingMessageHTTPS(false)) {
|
||||
this.socket.encrypted = true;
|
||||
}
|
||||
}
|
||||
|
||||
this._readableState.readingMore = true;
|
||||
}
|
||||
|
||||
function requestHasNoBody(method, req) {
|
||||
if ("GET" === method || "HEAD" === method || "TRACE" === method || "CONNECT" === method || "OPTIONS" === method)
|
||||
return true;
|
||||
const headers = req?.headers;
|
||||
const contentLength = headers?.["content-length"];
|
||||
if (!parseInt(contentLength, 10)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function onDataIncomingMessage(
|
||||
this: import("node:http").IncomingMessage,
|
||||
chunk,
|
||||
isLast,
|
||||
aborted: NodeHTTPResponseAbortEvent,
|
||||
) {
|
||||
if (aborted === NodeHTTPResponseAbortEvent.abort) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (chunk && !this._dumped) this.push(chunk);
|
||||
|
||||
if (isLast) {
|
||||
emitEOFIncomingMessage(this);
|
||||
}
|
||||
}
|
||||
|
||||
const IncomingMessagePrototype = {
|
||||
constructor: IncomingMessage,
|
||||
__proto__: Readable.prototype,
|
||||
_construct(callback) {
|
||||
// TODO: streaming
|
||||
const type = this[typeSymbol];
|
||||
|
||||
if (type === NodeHTTPIncomingRequestType.FetchResponse) {
|
||||
if (!webRequestOrResponseHasBodyValue(this[webRequestOrResponse])) {
|
||||
this.complete = true;
|
||||
this.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
// 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");
|
||||
const handle = this[kHandle];
|
||||
if (handle) {
|
||||
handle.ondata = undefined;
|
||||
}
|
||||
this.resume();
|
||||
}
|
||||
},
|
||||
_read(size) {
|
||||
if (!this._consuming) {
|
||||
this._readableState.readingMore = false;
|
||||
this._consuming = true;
|
||||
}
|
||||
|
||||
const socket = this.socket;
|
||||
if (socket && socket.readable) {
|
||||
//https://github.com/nodejs/node/blob/13e3aef053776be9be262f210dc438ecec4a3c8d/lib/_http_incoming.js#L211-L213
|
||||
socket.resume();
|
||||
}
|
||||
|
||||
if (this[eofInProgress]) {
|
||||
// There is a nextTick pending that will emit EOF
|
||||
return;
|
||||
}
|
||||
|
||||
let internalRequest;
|
||||
if (this[noBodySymbol]) {
|
||||
emitEOFIncomingMessage(this);
|
||||
return;
|
||||
} else if ((internalRequest = this[kHandle])) {
|
||||
const bodyReadState = internalRequest.hasBody;
|
||||
|
||||
if (
|
||||
(bodyReadState & NodeHTTPBodyReadState.done) !== 0 ||
|
||||
bodyReadState === NodeHTTPBodyReadState.none ||
|
||||
this._dumped
|
||||
) {
|
||||
emitEOFIncomingMessage(this);
|
||||
}
|
||||
|
||||
if ((bodyReadState & NodeHTTPBodyReadState.hasBufferedDataDuringPause) !== 0) {
|
||||
const drained = internalRequest.drainRequestBody();
|
||||
if (drained && !this._dumped) {
|
||||
this.push(drained);
|
||||
}
|
||||
}
|
||||
|
||||
if (!internalRequest.ondata) {
|
||||
internalRequest.ondata = onDataIncomingMessage.bind(this);
|
||||
internalRequest.hasCustomOnData = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (this[bodyStreamSymbol] == null) {
|
||||
// If it's all available right now, we skip going through ReadableStream.
|
||||
let completeBody = getCompleteWebRequestOrResponseBodyValueAsArrayBuffer(this[webRequestOrResponse]);
|
||||
if (completeBody) {
|
||||
$assert(completeBody instanceof ArrayBuffer, "completeBody is not an ArrayBuffer");
|
||||
$assert(completeBody.byteLength > 0, "completeBody should not be empty");
|
||||
|
||||
// They're ignoring the data. Let's not do anything with it.
|
||||
if (!this._dumped) {
|
||||
this.push(new Buffer(completeBody));
|
||||
}
|
||||
emitEOFIncomingMessage(this);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = this[webRequestOrResponse].body?.getReader?.() as ReadableStreamDefaultReader;
|
||||
if (!reader) {
|
||||
emitEOFIncomingMessage(this);
|
||||
return;
|
||||
}
|
||||
|
||||
this[bodyStreamSymbol] = reader;
|
||||
consumeStream(this, reader);
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
_finish() {
|
||||
this.emit("prefinish");
|
||||
},
|
||||
_destroy: function IncomingMessage_destroy(err, cb) {
|
||||
const shouldEmitAborted = !this.readableEnded || !this.complete;
|
||||
|
||||
if (shouldEmitAborted) {
|
||||
this[abortedSymbol] = true;
|
||||
// IncomingMessage emits 'aborted'.
|
||||
// Client emits 'abort'.
|
||||
this.emit("aborted");
|
||||
}
|
||||
|
||||
// Suppress "AbortError" from fetch() because we emit this in the 'aborted' event
|
||||
if (isAbortError(err)) {
|
||||
err = undefined;
|
||||
}
|
||||
|
||||
var nodeHTTPResponse = this[kHandle];
|
||||
if (nodeHTTPResponse) {
|
||||
this[kHandle] = undefined;
|
||||
nodeHTTPResponse.onabort = nodeHTTPResponse.ondata = undefined;
|
||||
if (!nodeHTTPResponse.finished && shouldEmitAborted) {
|
||||
nodeHTTPResponse.abort();
|
||||
}
|
||||
const socket = this.socket;
|
||||
if (socket && !socket.destroyed && shouldEmitAborted) {
|
||||
socket.destroy(err);
|
||||
}
|
||||
} else {
|
||||
const stream = this[bodyStreamSymbol];
|
||||
this[bodyStreamSymbol] = undefined;
|
||||
const streamState = stream?.$state;
|
||||
|
||||
if (streamState === $streamReadable || streamState === $streamWaiting || streamState === $streamWritable) {
|
||||
stream?.cancel?.().catch(nop);
|
||||
}
|
||||
|
||||
const socket = this[kFakeSocket];
|
||||
if (socket && !socket.destroyed && shouldEmitAborted) {
|
||||
socket.destroy(err);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isCallable(cb)) {
|
||||
emitErrorNextTickIfErrorListenerNT(this, err, cb);
|
||||
}
|
||||
},
|
||||
get aborted() {
|
||||
return this[abortedSymbol];
|
||||
},
|
||||
set aborted(value) {
|
||||
this[abortedSymbol] = value;
|
||||
},
|
||||
get connection() {
|
||||
return (this[kFakeSocket] ??= new FakeSocket());
|
||||
},
|
||||
get statusCode() {
|
||||
return this[statusCodeSymbol];
|
||||
},
|
||||
set statusCode(value) {
|
||||
if (!(value in STATUS_CODES)) return;
|
||||
this[statusCodeSymbol] = value;
|
||||
},
|
||||
get statusMessage() {
|
||||
return this[statusMessageSymbol];
|
||||
},
|
||||
set statusMessage(value) {
|
||||
this[statusMessageSymbol] = value;
|
||||
},
|
||||
get httpVersion() {
|
||||
return "1.1";
|
||||
},
|
||||
set httpVersion(value) {
|
||||
// noop
|
||||
},
|
||||
get httpVersionMajor() {
|
||||
return 1;
|
||||
},
|
||||
set httpVersionMajor(value) {
|
||||
// noop
|
||||
},
|
||||
get httpVersionMinor() {
|
||||
return 1;
|
||||
},
|
||||
set httpVersionMinor(value) {
|
||||
// noop
|
||||
},
|
||||
get rawTrailers() {
|
||||
return [];
|
||||
},
|
||||
set rawTrailers(value) {
|
||||
// noop
|
||||
},
|
||||
get trailers() {
|
||||
return kEmptyObject;
|
||||
},
|
||||
set trailers(value) {
|
||||
// noop
|
||||
},
|
||||
setTimeout(msecs, callback) {
|
||||
this.take;
|
||||
const req = this[kHandle] || this[webRequestOrResponse];
|
||||
|
||||
if (req) {
|
||||
setRequestTimeout(req, Math.ceil(msecs / 1000));
|
||||
typeof callback === "function" && this.once("timeout", callback);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
get socket() {
|
||||
return (this[kFakeSocket] ??= new FakeSocket());
|
||||
},
|
||||
set socket(value) {
|
||||
this[kFakeSocket] = value;
|
||||
},
|
||||
} satisfies typeof import("node:http").IncomingMessage.prototype;
|
||||
IncomingMessage.prototype = IncomingMessagePrototype;
|
||||
$setPrototypeDirect.$call(IncomingMessage, Readable);
|
||||
|
||||
function assignHeadersSlow(object, req) {
|
||||
const headers = req.headers;
|
||||
var outHeaders = Object.create(null);
|
||||
const rawHeaders: string[] = [];
|
||||
var i = 0;
|
||||
for (let key in headers) {
|
||||
var originalKey = key;
|
||||
var value = headers[originalKey];
|
||||
|
||||
key = key.toLowerCase();
|
||||
|
||||
if (key !== "set-cookie") {
|
||||
value = String(value);
|
||||
$putByValDirect(rawHeaders, i++, originalKey);
|
||||
$putByValDirect(rawHeaders, i++, value);
|
||||
outHeaders[key] = value;
|
||||
} else {
|
||||
if ($isJSArray(value)) {
|
||||
outHeaders[key] = value.slice();
|
||||
|
||||
for (let entry of value) {
|
||||
$putByValDirect(rawHeaders, i++, originalKey);
|
||||
$putByValDirect(rawHeaders, i++, entry);
|
||||
}
|
||||
} else {
|
||||
value = String(value);
|
||||
outHeaders[key] = [value];
|
||||
$putByValDirect(rawHeaders, i++, originalKey);
|
||||
$putByValDirect(rawHeaders, i++, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
object.headers = outHeaders;
|
||||
object.rawHeaders = rawHeaders;
|
||||
}
|
||||
|
||||
function assignHeaders(object, req) {
|
||||
// This fast path is an 8% speedup for a "hello world" node:http server, and a 7% speedup for a "hello world" express server
|
||||
if (assignHeadersFast(req, object, headersTuple)) {
|
||||
const headers = $getInternalField(headersTuple, 0);
|
||||
const rawHeaders = $getInternalField(headersTuple, 1);
|
||||
$putInternalField(headersTuple, 0, undefined);
|
||||
$putInternalField(headersTuple, 1, undefined);
|
||||
object.headers = headers;
|
||||
object.rawHeaders = rawHeaders;
|
||||
return true;
|
||||
} else {
|
||||
assignHeadersSlow(object, req);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
317
src/js/node/_http_outgoing.ts
Normal file
317
src/js/node/_http_outgoing.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import {
|
||||
getHeader,
|
||||
hasServerResponseFinished,
|
||||
headersSymbol,
|
||||
kAbortController,
|
||||
kEmptyObject,
|
||||
kFakeSocket,
|
||||
kHandle,
|
||||
kHeaderState,
|
||||
NodeHTTPHeaderState,
|
||||
setHeader,
|
||||
timeoutTimerSymbol,
|
||||
FakeSocket,
|
||||
kEmitState,
|
||||
ClientRequestEmitState,
|
||||
kBodyChunks,
|
||||
validateMsecs,
|
||||
} from "internal/http/share";
|
||||
import Stream from "node:stream";
|
||||
|
||||
const kUniqueHeaders = Symbol('kUniqueHeaders');
|
||||
const kHighWaterMark = Symbol('kHighWaterMark');
|
||||
|
||||
const {
|
||||
validateFunction,
|
||||
checkIsHttpToken,
|
||||
// validateLinkHeaderValue,
|
||||
// validateObject,
|
||||
// validateInteger,
|
||||
} = require("internal/validators");
|
||||
|
||||
const getRawKeys = $newCppFunction("JSFetchHeaders.cpp", "jsFetchHeaders_getRawKeys", 0);
|
||||
|
||||
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
|
||||
const RegExpPrototypeExec = RegExp.prototype.exec;
|
||||
/**
|
||||
* True if val contains an invalid field-vchar
|
||||
* field-value = *( field-content / obs-fold )
|
||||
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
||||
* field-vchar = VCHAR / obs-text
|
||||
*/
|
||||
function checkInvalidHeaderChar(val: string) {
|
||||
return RegExpPrototypeExec.$call(headerCharRegex, val) !== null;
|
||||
}
|
||||
|
||||
const validateHeaderName = (name, label?) => {
|
||||
if (typeof name !== "string" || !name || !checkIsHttpToken(name)) {
|
||||
throw $ERR_INVALID_HTTP_TOKEN(label || "Header name", name);
|
||||
}
|
||||
};
|
||||
|
||||
const validateHeaderValue = (name, value) => {
|
||||
if (value === undefined) {
|
||||
throw $ERR_HTTP_INVALID_HEADER_VALUE(value, name);
|
||||
}
|
||||
if (checkInvalidHeaderChar(value)) {
|
||||
throw $ERR_INVALID_CHAR("header content", name);
|
||||
}
|
||||
};
|
||||
|
||||
function OutgoingMessage(options): void {
|
||||
if (!new.target)
|
||||
return new OutgoingMessage(options);
|
||||
|
||||
Stream.$call(this, options);
|
||||
|
||||
this.sendDate = true;
|
||||
this.finished = false;
|
||||
this[kHeaderState] = NodeHTTPHeaderState.none;
|
||||
this[kAbortController] = null;
|
||||
|
||||
this.writable = true;
|
||||
this.destroyed = false;
|
||||
this._hasBody = true;
|
||||
this._trailer = "";
|
||||
this._contentLength = null;
|
||||
this._closed = false;
|
||||
this._header = null;
|
||||
this._headerSent = false;
|
||||
}
|
||||
|
||||
const OutgoingMessagePrototype = {
|
||||
constructor: OutgoingMessage,
|
||||
__proto__: Stream.prototype,
|
||||
|
||||
// These are fields which we do not use in our implementation, but are observable in Node.js.
|
||||
_keepAliveTimeout: 0,
|
||||
_defaultKeepAlive: true,
|
||||
shouldKeepAlive: true,
|
||||
_onPendingData: function nop() {},
|
||||
outputSize: 0,
|
||||
outputData: [],
|
||||
strictContentLength: false,
|
||||
_removedTE: false,
|
||||
_removedContLen: false,
|
||||
_removedConnection: false,
|
||||
usesChunkedEncodingByDefault: true,
|
||||
_closed: false,
|
||||
|
||||
appendHeader(name, value) {
|
||||
var headers = (this[headersSymbol] ??= new Headers());
|
||||
headers.append(name, value);
|
||||
return this;
|
||||
},
|
||||
|
||||
_implicitHeader() {
|
||||
throw $ERR_METHOD_NOT_IMPLEMENTED("_implicitHeader()");
|
||||
},
|
||||
flushHeaders() {},
|
||||
getHeader(name) {
|
||||
return getHeader(this[headersSymbol], name);
|
||||
},
|
||||
|
||||
// Overridden by ClientRequest and ServerResponse; this version will be called only if the user constructs OutgoingMessage directly.
|
||||
write(chunk, encoding, callback) {
|
||||
if ($isCallable(chunk)) {
|
||||
callback = chunk;
|
||||
chunk = undefined;
|
||||
} else if ($isCallable(encoding)) {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
} else if (!$isCallable(callback)) {
|
||||
callback = undefined;
|
||||
encoding = undefined;
|
||||
}
|
||||
hasServerResponseFinished(this, chunk, callback);
|
||||
if (chunk) {
|
||||
const len = Buffer.byteLength(chunk, encoding || (typeof chunk === "string" ? "utf8" : "buffer"));
|
||||
if (len > 0) {
|
||||
this.outputSize += len;
|
||||
this.outputData.push(chunk);
|
||||
}
|
||||
}
|
||||
return this.writableHighWaterMark >= this.outputSize;
|
||||
},
|
||||
|
||||
getHeaderNames() {
|
||||
var headers = this[headersSymbol];
|
||||
if (!headers) return [];
|
||||
return Array.from(headers.keys());
|
||||
},
|
||||
|
||||
getRawHeaderNames() {
|
||||
var headers = this[headersSymbol];
|
||||
if (!headers) return [];
|
||||
return getRawKeys.$call(headers);
|
||||
},
|
||||
|
||||
getHeaders() {
|
||||
const headers = this[headersSymbol];
|
||||
if (!headers) return kEmptyObject;
|
||||
return headers.toJSON();
|
||||
},
|
||||
|
||||
removeHeader(name) {
|
||||
if (this[kHeaderState] === NodeHTTPHeaderState.sent) {
|
||||
throw $ERR_HTTP_HEADERS_SENT("remove");
|
||||
}
|
||||
const headers = this[headersSymbol];
|
||||
if (!headers) return;
|
||||
headers.delete(name);
|
||||
},
|
||||
|
||||
setHeader(name, value) {
|
||||
validateHeaderName(name);
|
||||
const headers = (this[headersSymbol] ??= new Headers());
|
||||
setHeader(headers, name, value);
|
||||
return this;
|
||||
},
|
||||
|
||||
hasHeader(name) {
|
||||
const headers = this[headersSymbol];
|
||||
if (!headers) return false;
|
||||
return headers.has(name);
|
||||
},
|
||||
|
||||
get headers() {
|
||||
const headers = this[headersSymbol];
|
||||
if (!headers) return kEmptyObject;
|
||||
return headers.toJSON();
|
||||
},
|
||||
set headers(value) {
|
||||
this[headersSymbol] = new Headers(value);
|
||||
},
|
||||
|
||||
addTrailers(headers) {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
|
||||
setTimeout(msecs, callback) {
|
||||
if (this.destroyed) return this;
|
||||
|
||||
this.timeout = msecs = validateMsecs(msecs, "msecs");
|
||||
|
||||
// Attempt to clear an existing timer in both cases -
|
||||
// even if it will be rescheduled we don't want to leak an existing timer.
|
||||
clearTimeout(this[timeoutTimerSymbol]);
|
||||
|
||||
if (msecs === 0) {
|
||||
if (callback != null) {
|
||||
if (!$isCallable(callback)) validateFunction(callback, "callback");
|
||||
this.removeListener("timeout", callback);
|
||||
}
|
||||
|
||||
this[timeoutTimerSymbol] = undefined;
|
||||
} else {
|
||||
this[timeoutTimerSymbol] = setTimeout(onTimeout.bind(this), msecs).unref();
|
||||
|
||||
if (callback != null) {
|
||||
if (!$isCallable(callback)) validateFunction(callback, "callback");
|
||||
this.once("timeout", callback);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
get connection() {
|
||||
return this.socket;
|
||||
},
|
||||
|
||||
get socket() {
|
||||
this[kFakeSocket] = this[kFakeSocket] ?? new FakeSocket();
|
||||
return this[kFakeSocket];
|
||||
},
|
||||
|
||||
set socket(value) {
|
||||
this[kFakeSocket] = value;
|
||||
},
|
||||
|
||||
get chunkedEncoding() {
|
||||
return false;
|
||||
},
|
||||
|
||||
set chunkedEncoding(value) {
|
||||
// noop
|
||||
},
|
||||
|
||||
get writableObjectMode() {
|
||||
return false;
|
||||
},
|
||||
|
||||
get writableLength() {
|
||||
return 0;
|
||||
},
|
||||
|
||||
get writableHighWaterMark() {
|
||||
return 16 * 1024;
|
||||
},
|
||||
|
||||
get writableNeedDrain() {
|
||||
return !this.destroyed && !this.finished && this[kBodyChunks] && this[kBodyChunks].length > 0;
|
||||
},
|
||||
|
||||
get writableEnded() {
|
||||
return this.finished;
|
||||
},
|
||||
|
||||
get writableFinished() {
|
||||
return this.finished && !!(this[kEmitState] & (1 << ClientRequestEmitState.finish));
|
||||
},
|
||||
|
||||
_send(data, encoding, callback, byteLength) {
|
||||
if (this.destroyed) {
|
||||
return false;
|
||||
}
|
||||
return this.write(data, encoding, callback);
|
||||
},
|
||||
end(chunk, encoding, callback) {
|
||||
return this;
|
||||
},
|
||||
destroy(err?: Error) {
|
||||
if (this.destroyed) return this;
|
||||
const handle = this[kHandle];
|
||||
this.destroyed = true;
|
||||
if (handle) {
|
||||
handle.abort();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
};
|
||||
OutgoingMessage.prototype = OutgoingMessagePrototype;
|
||||
$setPrototypeDirect.$call(OutgoingMessage, Stream);
|
||||
|
||||
function onTimeout() {
|
||||
this[timeoutTimerSymbol] = undefined;
|
||||
this[kAbortController]?.abort();
|
||||
const handle = this[kHandle];
|
||||
|
||||
this.emit("timeout");
|
||||
if (handle) {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
function parseUniqueHeadersOption(headers: any) {
|
||||
if (!$isJSArray(headers)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unique = new Set();
|
||||
const l = headers.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
unique.$add(headers[i].toLowerCase());
|
||||
}
|
||||
|
||||
return unique;
|
||||
}
|
||||
|
||||
export {
|
||||
kHighWaterMark,
|
||||
kUniqueHeaders,
|
||||
parseUniqueHeadersOption,
|
||||
validateHeaderName,
|
||||
validateHeaderValue,
|
||||
OutgoingMessage,
|
||||
};
|
||||
1642
src/js/node/_http_server.ts
Normal file
1642
src/js/node/_http_server.ts
Normal file
File diff suppressed because it is too large
Load Diff
3817
src/js/node/http.ts
3817
src/js/node/http.ts
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user