mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
## Summary Fixes all oxlint `no-unused-expressions` violations across the codebase by: - Adding an oxlint override to disable the rule for `src/js/builtins/**`, where special syntax markers like `$getter`, `$constructor`, etc. are intentionally used as standalone expressions - Converting short-circuit expressions (`condition && fn()`) to proper if statements for improved code clarity - Wrapping intentional property access side effects (e.g., `this.stdio;`, `err.stack;`) with the `void` operator - Converting ternary expressions used for control flow to if/else statements ## Test plan - [x] `bun lint` passes with no errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1902 lines
54 KiB
TypeScript
1902 lines
54 KiB
TypeScript
// Hardcoded module "node:_http_server"
|
|
const EventEmitter: typeof import("node:events").EventEmitter = require("node:events");
|
|
const { Duplex, Stream } = require("node:stream");
|
|
const { _checkInvalidHeaderChar: checkInvalidHeaderChar } = require("node:_http_common");
|
|
const { validateObject, validateLinkHeaderValue, validateBoolean, validateInteger } = require("internal/validators");
|
|
|
|
const { isPrimary } = require("internal/cluster/isPrimary");
|
|
const { throwOnInvalidTLSArray } = require("internal/tls");
|
|
const {
|
|
kInternalSocketData,
|
|
serverSymbol,
|
|
kHandle,
|
|
kRealListen,
|
|
tlsSymbol,
|
|
optionsSymbol,
|
|
kDeferredTimeouts,
|
|
kDeprecatedReplySymbol,
|
|
headerStateSymbol,
|
|
NodeHTTPHeaderState,
|
|
kPendingCallbacks,
|
|
kRequest,
|
|
kCloseCallback,
|
|
NodeHTTPResponseFlags,
|
|
headersSymbol,
|
|
emitErrorNextTickIfErrorListenerNT,
|
|
getIsNextIncomingMessageHTTPS,
|
|
setIsNextIncomingMessageHTTPS,
|
|
callCloseCallback,
|
|
emitCloseNT,
|
|
ConnResetException,
|
|
NodeHTTPResponseAbortEvent,
|
|
STATUS_CODES,
|
|
isTlsSymbol,
|
|
hasServerResponseFinished,
|
|
OutgoingMessagePrototype,
|
|
NodeHTTPBodyReadState,
|
|
controllerSymbol,
|
|
firstWriteSymbol,
|
|
deferredSymbol,
|
|
eofInProgress,
|
|
runSymbol,
|
|
drainMicrotasks,
|
|
setServerIdleTimeout,
|
|
setServerCustomOptions,
|
|
getMaxHTTPHeaderSize,
|
|
} = require("internal/http");
|
|
const NumberIsNaN = Number.isNaN;
|
|
|
|
const { format } = require("internal/util/inspect");
|
|
|
|
const { IncomingMessage } = require("node:_http_incoming");
|
|
const { OutgoingMessage } = require("node:_http_outgoing");
|
|
const { kIncomingMessage } = require("node:_http_common");
|
|
const kConnectionsCheckingInterval = Symbol("http.server.connectionsCheckingInterval");
|
|
|
|
const getBunServerAllClosedPromise = $newZigFunction("node_http_binding.zig", "getBunServerAllClosedPromise", 1);
|
|
const sendHelper = $newZigFunction("node_cluster_binding.zig", "sendHelperChild", 3);
|
|
|
|
const kServerResponse = Symbol("ServerResponse");
|
|
const kRejectNonStandardBodyWrites = Symbol("kRejectNonStandardBodyWrites");
|
|
const GlobalPromise = globalThis.Promise;
|
|
const kEmptyBuffer = Buffer.alloc(0);
|
|
const ObjectKeys = Object.keys;
|
|
const MathMin = Math.min;
|
|
|
|
let cluster;
|
|
|
|
function emitCloseServer(self: Server) {
|
|
callCloseCallback(self);
|
|
self.emit("close");
|
|
}
|
|
function emitCloseNTServer(this: Server) {
|
|
process.nextTick(emitCloseServer, this);
|
|
}
|
|
|
|
function setCloseCallback(self, callback) {
|
|
if (callback === self[kCloseCallback]) {
|
|
return;
|
|
}
|
|
if (self[kCloseCallback]) {
|
|
throw new Error("Close callback already set");
|
|
}
|
|
self[kCloseCallback] = callback;
|
|
}
|
|
|
|
function assignSocketInternal(self, socket) {
|
|
if (socket._httpMessage) {
|
|
throw $ERR_HTTP_SOCKET_ASSIGNED("Socket already assigned");
|
|
}
|
|
socket._httpMessage = self;
|
|
setCloseCallback(socket, onServerResponseClose);
|
|
self.socket = socket;
|
|
self.emit("socket", socket);
|
|
}
|
|
|
|
function onServerResponseClose() {
|
|
// EventEmitter.emit makes a copy of the 'close' listeners array before
|
|
// calling the listeners. detachSocket() unregisters onServerResponseClose
|
|
// but if detachSocket() is called, directly or indirectly, by a 'close'
|
|
// listener, onServerResponseClose is still in that copy of the listeners
|
|
// array. That is, in the example below, b still gets called even though
|
|
// it's been removed by a:
|
|
//
|
|
// const EventEmitter = require('events');
|
|
// const obj = new EventEmitter();
|
|
// obj.on('event', a);
|
|
// obj.on('event', b);
|
|
// function a() { obj.removeListener('event', b) }
|
|
// function b() { throw "BAM!" }
|
|
// obj.emit('event'); // throws
|
|
//
|
|
// Ergo, we need to deal with stale 'close' events and handle the case
|
|
// where the ServerResponse object has already been deconstructed.
|
|
// Fortunately, that requires only a single if check. :-)
|
|
const httpMessage = this._httpMessage;
|
|
if (httpMessage) {
|
|
emitCloseNT(httpMessage);
|
|
}
|
|
}
|
|
|
|
function strictContentLength(response) {
|
|
if (response.strictContentLength) {
|
|
let contentLength = response._contentLength ?? response.getHeader("content-length");
|
|
if (
|
|
contentLength &&
|
|
response._hasBody &&
|
|
!response._removedContLen &&
|
|
!response.chunkedEncoding &&
|
|
!response.hasHeader("transfer-encoding")
|
|
) {
|
|
if (typeof contentLength === "number") {
|
|
return contentLength;
|
|
} else if (typeof contentLength === "string") {
|
|
contentLength = parseInt(contentLength, 10);
|
|
if (NumberIsNaN(contentLength)) {
|
|
return;
|
|
}
|
|
return contentLength;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const ServerResponse_writeDeprecated = function _write(chunk, encoding, callback) {
|
|
if ($isCallable(encoding)) {
|
|
callback = encoding;
|
|
encoding = undefined;
|
|
}
|
|
if (!$isCallable(callback)) {
|
|
callback = undefined;
|
|
}
|
|
if (encoding && encoding !== "buffer") {
|
|
chunk = Buffer.from(chunk, encoding);
|
|
}
|
|
if (this.destroyed || this.finished) {
|
|
if (chunk) {
|
|
emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_WRITE_AFTER_END(), callback);
|
|
}
|
|
return false;
|
|
}
|
|
if (this[firstWriteSymbol] === undefined && !this.headersSent) {
|
|
this[firstWriteSymbol] = chunk;
|
|
if (callback) callback();
|
|
return;
|
|
}
|
|
|
|
ensureReadableStreamController.$call(this, controller => {
|
|
controller.write(chunk);
|
|
if (callback) callback();
|
|
});
|
|
};
|
|
|
|
function onNodeHTTPServerSocketTimeout() {
|
|
const req = this[kRequest];
|
|
const reqTimeout = req && !req.complete && req.emit("timeout", this);
|
|
const res = this._httpMessage;
|
|
const resTimeout = res && res.emit("timeout", this);
|
|
const serverTimeout = this.server.emit("timeout", this);
|
|
|
|
if (!reqTimeout && !resTimeout && !serverTimeout) this.destroy();
|
|
}
|
|
|
|
function emitRequestCloseNT(self) {
|
|
callCloseCallback(self);
|
|
self.emit("close");
|
|
}
|
|
|
|
function emitListeningNextTick(self, hostname, port) {
|
|
if ((self.listening = !!self[serverSymbol])) {
|
|
// TODO: remove the arguments
|
|
// Note does not pass any arguments.
|
|
self.emit("listening", null, hostname, port);
|
|
}
|
|
}
|
|
|
|
function Server(options, callback): void {
|
|
if (!(this instanceof Server)) return new Server(options, callback);
|
|
EventEmitter.$call(this);
|
|
this[kConnectionsCheckingInterval] = { _destroyed: false };
|
|
|
|
this.listening = false;
|
|
this._unref = false;
|
|
this.maxRequestsPerSocket = 0;
|
|
this[kInternalSocketData] = undefined;
|
|
this[tlsSymbol] = null;
|
|
this.noDelay = true;
|
|
if (typeof options === "function") {
|
|
callback = options;
|
|
options = {};
|
|
} else if (options == null) {
|
|
options = {};
|
|
} else {
|
|
validateObject(options, "options");
|
|
options = { ...options };
|
|
|
|
let cert = options.cert;
|
|
if (cert) {
|
|
throwOnInvalidTLSArray("options.cert", cert);
|
|
this[isTlsSymbol] = true;
|
|
}
|
|
|
|
let key = options.key;
|
|
if (key) {
|
|
throwOnInvalidTLSArray("options.key", key);
|
|
this[isTlsSymbol] = true;
|
|
}
|
|
|
|
let ca = options.ca;
|
|
if (ca) {
|
|
throwOnInvalidTLSArray("options.ca", ca);
|
|
this[isTlsSymbol] = true;
|
|
}
|
|
|
|
let passphrase = options.passphrase;
|
|
if (passphrase && typeof passphrase !== "string") {
|
|
throw $ERR_INVALID_ARG_TYPE("options.passphrase", "string", passphrase);
|
|
}
|
|
|
|
let serverName = options.servername;
|
|
if (serverName && typeof serverName !== "string") {
|
|
throw $ERR_INVALID_ARG_TYPE("options.servername", "string", serverName);
|
|
}
|
|
|
|
let secureOptions = options.secureOptions || 0;
|
|
if (secureOptions && typeof secureOptions !== "number") {
|
|
throw $ERR_INVALID_ARG_TYPE("options.secureOptions", "number", secureOptions);
|
|
}
|
|
|
|
if (this[isTlsSymbol]) {
|
|
this[tlsSymbol] = {
|
|
serverName,
|
|
key,
|
|
cert,
|
|
ca,
|
|
passphrase,
|
|
secureOptions,
|
|
};
|
|
} else {
|
|
this[tlsSymbol] = null;
|
|
}
|
|
}
|
|
|
|
this[optionsSymbol] = options;
|
|
storeHTTPOptions.$call(this, options);
|
|
|
|
if (callback) this.on("request", callback);
|
|
return this;
|
|
}
|
|
$toClass(Server, "Server", EventEmitter);
|
|
|
|
Server.prototype[kIncomingMessage] = undefined;
|
|
|
|
Server.prototype[kServerResponse] = undefined;
|
|
|
|
Server.prototype[kConnectionsCheckingInterval] = undefined;
|
|
|
|
Server.prototype.ref = function () {
|
|
this._unref = false;
|
|
this[serverSymbol]?.ref?.();
|
|
return this;
|
|
};
|
|
|
|
Server.prototype.unref = function () {
|
|
this._unref = true;
|
|
this[serverSymbol]?.unref?.();
|
|
return this;
|
|
};
|
|
|
|
Server.prototype.closeAllConnections = function () {
|
|
const server = this[serverSymbol];
|
|
if (!server) {
|
|
return;
|
|
}
|
|
this[serverSymbol] = undefined;
|
|
const connectionsCheckingInterval = this[kConnectionsCheckingInterval];
|
|
if (connectionsCheckingInterval) {
|
|
connectionsCheckingInterval._destroyed = true;
|
|
}
|
|
this.listening = false;
|
|
|
|
server.stop(true);
|
|
};
|
|
|
|
Server.prototype.closeIdleConnections = function () {
|
|
const server = this[serverSymbol];
|
|
server.closeIdleConnections();
|
|
};
|
|
|
|
Server.prototype.close = function (optionalCallback?) {
|
|
const server = this[serverSymbol];
|
|
if (!server) {
|
|
if (typeof optionalCallback === "function") process.nextTick(optionalCallback, $ERR_SERVER_NOT_RUNNING());
|
|
return;
|
|
}
|
|
this[serverSymbol] = undefined;
|
|
const connectionsCheckingInterval = this[kConnectionsCheckingInterval];
|
|
if (connectionsCheckingInterval) {
|
|
connectionsCheckingInterval._destroyed = true;
|
|
}
|
|
if (typeof optionalCallback === "function") setCloseCallback(this, optionalCallback);
|
|
this.listening = false;
|
|
server.closeIdleConnections();
|
|
server.stop();
|
|
};
|
|
|
|
Server.prototype[EventEmitter.captureRejectionSymbol] = function (err, event, ...args) {
|
|
switch (event) {
|
|
case "request": {
|
|
const { 1: res } = args;
|
|
if (!res.headersSent && !res.writableEnded) {
|
|
// Don't leak headers.
|
|
const names = res.getHeaderNames();
|
|
for (let i = 0; i < names.length; i++) {
|
|
res.removeHeader(names[i]);
|
|
}
|
|
res.statusCode = 500;
|
|
res.end(STATUS_CODES[500]);
|
|
} else {
|
|
res.destroy();
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
// net.Server.prototype[EventEmitter.captureRejectionSymbol].apply(this, arguments);
|
|
// .apply(this, arguments);
|
|
const { 1: res } = args;
|
|
res?.socket?.destroy();
|
|
break;
|
|
}
|
|
};
|
|
|
|
Server.prototype[Symbol.asyncDispose] = function () {
|
|
const { resolve, reject, promise } = Promise.withResolvers();
|
|
this.close(function (err, ...args) {
|
|
if (err) {
|
|
reject(err);
|
|
} else resolve(...args);
|
|
});
|
|
return promise;
|
|
};
|
|
|
|
Server.prototype.address = function () {
|
|
if (!this[serverSymbol]) return null;
|
|
return this[serverSymbol].address;
|
|
};
|
|
|
|
Server.prototype.listen = function () {
|
|
const server = this;
|
|
let port, host, onListen;
|
|
let socketPath;
|
|
let tls = this[tlsSymbol];
|
|
|
|
// This logic must align with:
|
|
// - https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/net.js#L274-L307
|
|
if (arguments.length > 0) {
|
|
if (($isObject(arguments[0]) || $isCallable(arguments[0])) && arguments[0] !== null) {
|
|
// (options[...][, cb])
|
|
port = arguments[0].port;
|
|
host = arguments[0].host;
|
|
socketPath = arguments[0].path;
|
|
|
|
const otherTLS = arguments[0].tls;
|
|
if (otherTLS && $isObject(otherTLS)) {
|
|
tls = otherTLS;
|
|
}
|
|
} else if (typeof arguments[0] === "string" && !(Number(arguments[0]) >= 0)) {
|
|
// (path[...][, cb])
|
|
socketPath = arguments[0];
|
|
} else {
|
|
// ([port][, host][...][, cb])
|
|
port = arguments[0];
|
|
if (arguments.length > 1 && typeof arguments[1] === "string") {
|
|
host = arguments[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bun defaults to port 3000.
|
|
// Node defaults to port 0.
|
|
if (port === undefined && !socketPath) {
|
|
port = 0;
|
|
}
|
|
|
|
if (typeof port === "string") {
|
|
const portNumber = parseInt(port);
|
|
if (!Number.isNaN(portNumber)) {
|
|
port = portNumber;
|
|
}
|
|
}
|
|
|
|
if ($isCallable(arguments[arguments.length - 1])) {
|
|
onListen = arguments[arguments.length - 1];
|
|
}
|
|
|
|
try {
|
|
// listenInCluster
|
|
|
|
if (isPrimary) {
|
|
server[kRealListen](tls, port, host, socketPath, false, onListen);
|
|
return this;
|
|
}
|
|
|
|
if (cluster === undefined) cluster = require("node:cluster");
|
|
|
|
// TODO: our net.Server and http.Server use different Bun APIs and our IPC doesnt support sending and receiving handles yet. use reusePort instead for now.
|
|
|
|
// const serverQuery = {
|
|
// // address: address,
|
|
// port: port,
|
|
// addressType: 4,
|
|
// // fd: fd,
|
|
// // flags,
|
|
// // backlog,
|
|
// // ...options,
|
|
// };
|
|
// cluster._getServer(server, serverQuery, function listenOnPrimaryHandle(err, handle) {
|
|
// // err = checkBindError(err, port, handle);
|
|
// // if (err) {
|
|
// // throw new ExceptionWithHostPort(err, "bind", address, port);
|
|
// // }
|
|
// if (err) {
|
|
// throw err;
|
|
// }
|
|
// server[kRealListen](port, host, socketPath, onListen);
|
|
// });
|
|
|
|
server.once("listening", () => {
|
|
cluster.worker.state = "listening";
|
|
const address = server.address();
|
|
const message = {
|
|
act: "listening",
|
|
port: (address && address.port) || port,
|
|
data: null,
|
|
addressType: 4,
|
|
};
|
|
sendHelper(message, null);
|
|
});
|
|
|
|
server[kRealListen](tls, port, host, socketPath, true, onListen);
|
|
} catch (err) {
|
|
setTimeout(() => server.emit("error", err), 1);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort, onListen) {
|
|
{
|
|
const ResponseClass = this[optionsSymbol].ServerResponse || ServerResponse;
|
|
const RequestClass = this[optionsSymbol].IncomingMessage || IncomingMessage;
|
|
const canUseInternalAssignSocket = ResponseClass?.prototype.assignSocket === ServerResponse.prototype.assignSocket;
|
|
let isHTTPS = false;
|
|
let server = this;
|
|
|
|
if (tls) {
|
|
this.serverName = tls.serverName || host || "localhost";
|
|
}
|
|
this[serverSymbol] = Bun.serve<any>({
|
|
idleTimeout: 0, // nodejs dont have a idleTimeout by default
|
|
tls,
|
|
port,
|
|
hostname: host,
|
|
unix: socketPath,
|
|
reusePort,
|
|
// Bindings to be used for WS Server
|
|
websocket: {
|
|
open(ws) {
|
|
ws.data.open(ws);
|
|
},
|
|
message(ws, message) {
|
|
ws.data.message(ws, message);
|
|
},
|
|
close(ws, code, reason) {
|
|
ws.data.close(ws, code, reason);
|
|
},
|
|
drain(ws) {
|
|
ws.data.drain(ws);
|
|
},
|
|
ping(ws, data) {
|
|
ws.data.ping(ws, data);
|
|
},
|
|
pong(ws, data) {
|
|
ws.data.pong(ws, data);
|
|
},
|
|
},
|
|
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
|
|
|
onNodeHTTPRequest(
|
|
bunServer,
|
|
url: string,
|
|
method: string,
|
|
headersObject: Record<string, string>,
|
|
headersArray: string[],
|
|
handle,
|
|
hasBody: boolean,
|
|
socketHandle,
|
|
isSocketNew,
|
|
socket,
|
|
isAncientHTTP: boolean,
|
|
) {
|
|
const prevIsNextIncomingMessageHTTPS = getIsNextIncomingMessageHTTPS();
|
|
setIsNextIncomingMessageHTTPS(isHTTPS);
|
|
if (!socket) {
|
|
socket = new NodeHTTPServerSocket(server, socketHandle, !!tls);
|
|
}
|
|
|
|
const http_req = new RequestClass(kHandle, url, method, headersObject, headersArray, handle, hasBody, socket);
|
|
if (isAncientHTTP) {
|
|
http_req.httpVersion = "1.0";
|
|
}
|
|
if (method === "CONNECT") {
|
|
// Handle CONNECT method for HTTP tunneling/proxy
|
|
if (server.listenerCount("connect") > 0) {
|
|
// For CONNECT, emit the event and let the handler respond
|
|
// Don't assign the socket to a response for CONNECT
|
|
// The handler should write the raw response
|
|
socket[kEnableStreaming](true);
|
|
const { promise, resolve } = $newPromiseCapability(Promise);
|
|
socket.once("close", resolve);
|
|
server.emit("connect", http_req, socket, kEmptyBuffer);
|
|
return promise;
|
|
} else {
|
|
// Node.js will close the socket and will NOT respond with 400 Bad Request
|
|
socketHandle.close();
|
|
}
|
|
return;
|
|
}
|
|
socket[kEnableStreaming](false);
|
|
|
|
const http_res = new ResponseClass(http_req, {
|
|
[kHandle]: handle,
|
|
[kRejectNonStandardBodyWrites]: server.rejectNonStandardBodyWrites,
|
|
});
|
|
|
|
setIsNextIncomingMessageHTTPS(prevIsNextIncomingMessageHTTPS);
|
|
handle.onabort = onServerRequestEvent.bind(socket);
|
|
// start buffering data if any, the user will need to resume() or .on("data") to read it
|
|
if (hasBody) {
|
|
handle.pause();
|
|
}
|
|
drainMicrotasks();
|
|
|
|
let resolveFunction;
|
|
let didFinish = false;
|
|
|
|
const isRequestsLimitSet = typeof server.maxRequestsPerSocket === "number" && server.maxRequestsPerSocket > 0;
|
|
let reachedRequestsLimit = false;
|
|
if (isRequestsLimitSet) {
|
|
const requestCount = (socket._requestCount || 0) + 1;
|
|
socket._requestCount = requestCount;
|
|
if (server.maxRequestsPerSocket < requestCount) {
|
|
reachedRequestsLimit = true;
|
|
}
|
|
}
|
|
|
|
if (isSocketNew && !reachedRequestsLimit) {
|
|
server.emit("connection", socket);
|
|
}
|
|
|
|
socket[kRequest] = http_req;
|
|
const is_upgrade = http_req.headers.upgrade;
|
|
if (!is_upgrade) {
|
|
if (canUseInternalAssignSocket) {
|
|
// ~10% performance improvement in JavaScriptCore due to avoiding .once("close", ...) and removing a listener
|
|
assignSocketInternal(http_res, socket);
|
|
} else {
|
|
http_res.assignSocket(socket);
|
|
}
|
|
}
|
|
function onClose() {
|
|
didFinish = true;
|
|
if (resolveFunction) resolveFunction();
|
|
}
|
|
|
|
setCloseCallback(http_res, onClose);
|
|
if (reachedRequestsLimit) {
|
|
server.emit("dropRequest", http_req, socket);
|
|
http_res.writeHead(503);
|
|
http_res.end();
|
|
socket.destroy();
|
|
} else if (is_upgrade) {
|
|
server.emit("upgrade", http_req, socket, kEmptyBuffer);
|
|
if (!socket._httpMessage) {
|
|
if (canUseInternalAssignSocket) {
|
|
// ~10% performance improvement in JavaScriptCore due to avoiding .once("close", ...) and removing a listener
|
|
assignSocketInternal(http_res, socket);
|
|
} else {
|
|
http_res.assignSocket(socket);
|
|
}
|
|
}
|
|
} else if (http_req.headers.expect !== undefined) {
|
|
if (http_req.headers.expect === "100-continue") {
|
|
if (server.listenerCount("checkContinue") > 0) {
|
|
server.emit("checkContinue", http_req, http_res);
|
|
} else {
|
|
http_res.writeContinue();
|
|
server.emit("request", http_req, http_res);
|
|
}
|
|
} else if (server.listenerCount("checkExpectation") > 0) {
|
|
server.emit("checkExpectation", http_req, http_res);
|
|
} else {
|
|
http_res.writeHead(417);
|
|
http_res.end();
|
|
}
|
|
} else {
|
|
server.emit("request", http_req, http_res);
|
|
}
|
|
|
|
socket.cork();
|
|
|
|
if (handle.finished || didFinish) {
|
|
handle = undefined;
|
|
http_res[kCloseCallback] = undefined;
|
|
http_res.detachSocket(socket);
|
|
return;
|
|
}
|
|
if (http_res.socket) {
|
|
http_res.on("finish", http_res.detachSocket.bind(http_res, socket));
|
|
}
|
|
|
|
const { resolve, promise } = $newPromiseCapability(Promise);
|
|
resolveFunction = resolve;
|
|
|
|
return promise;
|
|
},
|
|
|
|
// Be very careful not to access (web) Request object
|
|
// properties:
|
|
// - request.url
|
|
// - request.headers
|
|
//
|
|
// We want to avoid triggering the getter for these properties because
|
|
// that will cause the data to be cloned twice, which costs memory & performance.
|
|
// fetch(req, _server) {
|
|
// var pendingResponse;
|
|
// var pendingError;
|
|
// var reject = err => {
|
|
// if (pendingError) return;
|
|
// pendingError = err;
|
|
// if (rejectFunction) rejectFunction(err);
|
|
// };
|
|
// var reply = function (resp) {
|
|
// if (pendingResponse) return;
|
|
// pendingResponse = resp;
|
|
// if (resolveFunction) resolveFunction(resp);
|
|
// };
|
|
// const prevIsNextIncomingMessageHTTPS = isNextIncomingMessageHTTPS;
|
|
// isNextIncomingMessageHTTPS = isHTTPS;
|
|
// const http_req = new RequestClass(req, {
|
|
// [typeSymbol]: NodeHTTPIncomingRequestType.FetchRequest,
|
|
// });
|
|
// assignEventCallback(req, onRequestEvent.bind(http_req));
|
|
// isNextIncomingMessageHTTPS = prevIsNextIncomingMessageHTTPS;
|
|
|
|
// const upgrade = http_req.headers.upgrade;
|
|
// const http_res = new ResponseClass(http_req, { [kDeprecatedReplySymbol]: reply });
|
|
// http_req.socket[kInternalSocketData] = [server, http_res, req];
|
|
// server.emit("connection", http_req.socket);
|
|
// const rejectFn = err => reject(err);
|
|
// http_req.once("error", rejectFn);
|
|
// http_res.once("error", rejectFn);
|
|
// if (upgrade) {
|
|
// server.emit("upgrade", http_req, http_req.socket, kEmptyBuffer);
|
|
// } else {
|
|
// server.emit("request", http_req, http_res);
|
|
// }
|
|
|
|
// if (pendingError) {
|
|
// throw pendingError;
|
|
// }
|
|
|
|
// if (pendingResponse) {
|
|
// return pendingResponse;
|
|
// }
|
|
|
|
// var { promise, resolve: resolveFunction, reject: rejectFunction } = $newPromiseCapability(GlobalPromise);
|
|
// return promise;
|
|
// },
|
|
});
|
|
|
|
getBunServerAllClosedPromise(this[serverSymbol]).$then(emitCloseNTServer.bind(this));
|
|
isHTTPS = this[serverSymbol].protocol === "https";
|
|
// always set strict method validation to true for node.js compatibility
|
|
setServerCustomOptions(
|
|
this[serverSymbol],
|
|
this.requireHostHeader,
|
|
true,
|
|
typeof this.maxHeaderSize !== "undefined" ? this.maxHeaderSize : getMaxHTTPHeaderSize(),
|
|
onServerClientError.bind(this),
|
|
);
|
|
|
|
if (this?._unref) {
|
|
this[serverSymbol]?.unref?.();
|
|
}
|
|
|
|
if ($isCallable(onListen)) {
|
|
this.once("listening", onListen);
|
|
}
|
|
|
|
if (this[kDeferredTimeouts]) {
|
|
for (const { msecs, callback } of this[kDeferredTimeouts]) {
|
|
this.setTimeout(msecs, callback);
|
|
}
|
|
delete this[kDeferredTimeouts];
|
|
}
|
|
|
|
setTimeout(emitListeningNextTick, 1, this, this[serverSymbol]?.hostname, this[serverSymbol]?.port);
|
|
}
|
|
};
|
|
|
|
Server.prototype.setTimeout = function (msecs, callback) {
|
|
const server = this[serverSymbol];
|
|
if (server) {
|
|
setServerIdleTimeout(server, Math.ceil(msecs / 1000));
|
|
if (typeof callback === "function") this.once("timeout", callback);
|
|
} else {
|
|
(this[kDeferredTimeouts] ??= []).push({ msecs, callback });
|
|
}
|
|
return this;
|
|
};
|
|
|
|
function onServerRequestEvent(this: NodeHTTPServerSocket, event: NodeHTTPResponseAbortEvent) {
|
|
const socket: NodeHTTPServerSocket = this;
|
|
switch (event) {
|
|
case NodeHTTPResponseAbortEvent.abort: {
|
|
if (!socket.destroyed) {
|
|
socket.destroy();
|
|
}
|
|
break;
|
|
}
|
|
case NodeHTTPResponseAbortEvent.timeout: {
|
|
socket.emit("timeout");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// uWS::HttpParserError
|
|
enum HttpParserError {
|
|
HTTP_PARSER_ERROR_NONE = 0,
|
|
HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING = 1,
|
|
HTTP_PARSER_ERROR_INVALID_CONTENT_LENGTH = 2,
|
|
HTTP_PARSER_ERROR_INVALID_TRANSFER_ENCODING = 3,
|
|
HTTP_PARSER_ERROR_MISSING_HOST_HEADER = 4,
|
|
HTTP_PARSER_ERROR_INVALID_REQUEST = 5,
|
|
HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE = 6,
|
|
HTTP_PARSER_ERROR_INVALID_HTTP_VERSION = 7,
|
|
HTTP_PARSER_ERROR_INVALID_EOF = 8,
|
|
HTTP_PARSER_ERROR_INVALID_METHOD = 9,
|
|
HTTP_PARSER_ERROR_INVALID_HEADER_TOKEN = 10,
|
|
}
|
|
function onServerClientError(ssl: boolean, socket: unknown, errorCode: number, rawPacket: ArrayBuffer) {
|
|
const self = this as Server;
|
|
let err;
|
|
switch (errorCode) {
|
|
case HttpParserError.HTTP_PARSER_ERROR_INVALID_CONTENT_LENGTH:
|
|
err = $HPE_UNEXPECTED_CONTENT_LENGTH("Parse Error");
|
|
break;
|
|
case HttpParserError.HTTP_PARSER_ERROR_INVALID_TRANSFER_ENCODING:
|
|
err = $HPE_INVALID_TRANSFER_ENCODING("Parse Error");
|
|
break;
|
|
case HttpParserError.HTTP_PARSER_ERROR_INVALID_EOF:
|
|
err = $HPE_INVALID_EOF_STATE("Parse Error");
|
|
break;
|
|
case HttpParserError.HTTP_PARSER_ERROR_INVALID_METHOD:
|
|
err = $HPE_INVALID_METHOD("Parse Error: Invalid method encountered");
|
|
err.bytesParsed = 1; // always 1 for now because is the first byte of the request line
|
|
break;
|
|
case HttpParserError.HTTP_PARSER_ERROR_INVALID_HEADER_TOKEN:
|
|
err = $HPE_INVALID_HEADER_TOKEN("Parse Error: Invalid header token encountered");
|
|
break;
|
|
case HttpParserError.HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE:
|
|
err = $HPE_HEADER_OVERFLOW("Parse Error: Header overflow");
|
|
err.bytesParsed = rawPacket.byteLength;
|
|
break;
|
|
default:
|
|
err = $HPE_INTERNAL("Parse Error");
|
|
break;
|
|
}
|
|
err.rawPacket = rawPacket;
|
|
const nodeSocket = new NodeHTTPServerSocket(self, socket, ssl);
|
|
self.emit("connection", nodeSocket);
|
|
self.emit("clientError", err, nodeSocket);
|
|
if (nodeSocket.listenerCount("error") > 0) {
|
|
nodeSocket.emit("error", err);
|
|
}
|
|
}
|
|
|
|
const kBytesWritten = Symbol("kBytesWritten");
|
|
const kEnableStreaming = Symbol("kEnableStreaming");
|
|
const NodeHTTPServerSocket = class Socket extends Duplex {
|
|
bytesRead = 0;
|
|
connecting = false;
|
|
timeout = 0;
|
|
[kBytesWritten] = 0;
|
|
[kHandle];
|
|
server: Server;
|
|
_httpMessage;
|
|
_secureEstablished = false;
|
|
#pendingCallback = null;
|
|
constructor(server: Server, handle, encrypted) {
|
|
super();
|
|
this.server = server;
|
|
this[kHandle] = handle;
|
|
this._secureEstablished = !!handle?.secureEstablished;
|
|
handle.onclose = this.#onClose.bind(this);
|
|
handle.duplex = this;
|
|
|
|
this.encrypted = encrypted;
|
|
this.on("timeout", onNodeHTTPServerSocketTimeout);
|
|
}
|
|
|
|
get bytesWritten() {
|
|
const handle = this[kHandle];
|
|
return handle
|
|
? (handle.response?.getBytesWritten?.() ?? handle.bytesWritten ?? this[kBytesWritten] ?? 0)
|
|
: (this[kBytesWritten] ?? 0);
|
|
}
|
|
set bytesWritten(value) {
|
|
this[kBytesWritten] = value;
|
|
}
|
|
|
|
[kEnableStreaming](enable: boolean) {
|
|
const handle = this[kHandle];
|
|
if (handle) {
|
|
if (enable) {
|
|
handle.ondata = this.#onData.bind(this);
|
|
handle.ondrain = this.#onDrain.bind(this);
|
|
} else {
|
|
handle.ondata = undefined;
|
|
handle.ondrain = undefined;
|
|
}
|
|
}
|
|
}
|
|
#onDrain() {
|
|
const handle = this[kHandle];
|
|
this[kBytesWritten] = handle ? (handle.response?.getBytesWritten?.() ?? handle.bytesWritten ?? 0) : 0;
|
|
const callback = this.#pendingCallback;
|
|
if (callback) {
|
|
this.#pendingCallback = null;
|
|
(callback as Function)();
|
|
}
|
|
this.emit("drain");
|
|
}
|
|
#onData(chunk, last) {
|
|
if (chunk) {
|
|
this.push(chunk);
|
|
}
|
|
if (last) {
|
|
const handle = this[kHandle];
|
|
if (handle) {
|
|
handle.ondata = undefined;
|
|
}
|
|
|
|
this.push(null);
|
|
}
|
|
}
|
|
#closeHandle(handle, callback) {
|
|
this[kHandle] = undefined;
|
|
handle.onclose = this.#onCloseForDestroy.bind(this, callback);
|
|
handle.close();
|
|
// lets sync check and destroy the request if it's not complete
|
|
const message = this._httpMessage;
|
|
const req = message?.req;
|
|
if (req && !req.complete) {
|
|
// at this point the handle is not destroyed yet, lets destroy the request
|
|
req.destroy();
|
|
}
|
|
}
|
|
#onClose() {
|
|
this[kHandle] = null;
|
|
|
|
const message = this._httpMessage;
|
|
const req = message?.req;
|
|
|
|
if (req && !req.complete && !req[kHandle]?.upgraded) {
|
|
// At this point the socket is already destroyed; let's avoid UAF
|
|
req[kHandle] = undefined;
|
|
if (req.listenerCount("error") > 0) {
|
|
req.destroy(new ConnResetException("aborted"));
|
|
} else {
|
|
req.destroy();
|
|
}
|
|
}
|
|
this.emit("close");
|
|
}
|
|
#onCloseForDestroy(closeCallback) {
|
|
this.#onClose();
|
|
if ($isCallable(closeCallback)) closeCallback();
|
|
}
|
|
|
|
_onTimeout() {
|
|
const handle = this[kHandle];
|
|
const response = handle?.response;
|
|
// If there is a response, and it has pending data,
|
|
// we suppress the timeout because a write is in progress.
|
|
if (response && response.writableLength > 0) {
|
|
return;
|
|
}
|
|
this.emit("timeout");
|
|
}
|
|
_unrefTimer() {
|
|
// for compatibility
|
|
}
|
|
|
|
address() {
|
|
return this[kHandle]?.remoteAddress || null;
|
|
}
|
|
|
|
get bufferSize() {
|
|
return this.writableLength;
|
|
}
|
|
|
|
connect(_port, _host, _connectListener) {
|
|
return this;
|
|
}
|
|
|
|
_destroy(err, callback) {
|
|
const handle = this[kHandle];
|
|
if (!handle) {
|
|
if ($isCallable(callback)) callback(err);
|
|
return;
|
|
}
|
|
handle.ondata = undefined;
|
|
if (handle.closed) {
|
|
const onclose = handle.onclose;
|
|
handle.onclose = undefined;
|
|
if ($isCallable(onclose)) {
|
|
onclose.$call(handle);
|
|
}
|
|
if ($isCallable(callback)) callback(err);
|
|
return;
|
|
}
|
|
|
|
this.#closeHandle(handle, callback);
|
|
}
|
|
|
|
_final(callback) {
|
|
const handle = this[kHandle];
|
|
if (!handle) {
|
|
callback();
|
|
return;
|
|
}
|
|
handle.end();
|
|
callback();
|
|
}
|
|
|
|
get localAddress() {
|
|
return this[kHandle]?.localAddress?.address;
|
|
}
|
|
|
|
get localFamily() {
|
|
return this[kHandle]?.localAddress?.family;
|
|
}
|
|
|
|
get localPort() {
|
|
return this[kHandle]?.localAddress?.port;
|
|
}
|
|
|
|
get pending() {
|
|
return this.connecting;
|
|
}
|
|
|
|
#resumeSocket() {
|
|
const handle = this[kHandle];
|
|
const response = handle?.response;
|
|
if (response) {
|
|
const resumed = response.resume();
|
|
if (resumed && resumed !== true) {
|
|
const bodyReadState = handle.hasBody;
|
|
|
|
const message = this._httpMessage;
|
|
const req = message?.req;
|
|
|
|
if ((bodyReadState & NodeHTTPBodyReadState.done) !== 0) {
|
|
emitServerSocketEOFNT(this, req);
|
|
}
|
|
if (req) {
|
|
req.push(resumed);
|
|
}
|
|
this.push(resumed);
|
|
}
|
|
}
|
|
}
|
|
|
|
_read(_size) {
|
|
// https://github.com/nodejs/node/blob/13e3aef053776be9be262f210dc438ecec4a3c8d/lib/net.js#L725-L737
|
|
this.#resumeSocket();
|
|
}
|
|
|
|
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) {
|
|
return this;
|
|
}
|
|
|
|
setEncoding(_encoding) {
|
|
const err = new Error("Changing the socket encoding is not allowed per RFC7230 Section 3.");
|
|
err.code = "ERR_HTTP_SOCKET_ENCODING";
|
|
throw err;
|
|
}
|
|
|
|
unref() {
|
|
return this;
|
|
}
|
|
|
|
_write(_chunk, _encoding, _callback) {
|
|
const handle = this[kHandle];
|
|
// only enable writting if we can drain
|
|
let err;
|
|
try {
|
|
if (handle && handle.ondrain && !handle.write(_chunk, _encoding)) {
|
|
this.#pendingCallback = _callback;
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
if (err) _callback(err);
|
|
else _callback();
|
|
}
|
|
|
|
pause() {
|
|
const handle = this[kHandle];
|
|
const response = handle?.response;
|
|
if (response) {
|
|
response.pause();
|
|
}
|
|
|
|
return super.pause();
|
|
}
|
|
|
|
resume() {
|
|
this.#resumeSocket();
|
|
return super.resume();
|
|
}
|
|
|
|
get [kInternalSocketData]() {
|
|
return this[kHandle]?.response;
|
|
}
|
|
} as unknown as typeof import("node:net").Socket;
|
|
|
|
function _writeHead(statusCode, reason, obj, response) {
|
|
const originalStatusCode = statusCode;
|
|
let hasContentLength = response.hasHeader("content-length");
|
|
statusCode |= 0;
|
|
if (statusCode < 100 || statusCode > 999) {
|
|
throw $ERR_HTTP_INVALID_STATUS_CODE(format("%s", originalStatusCode));
|
|
}
|
|
|
|
if (typeof reason === "string") {
|
|
// writeHead(statusCode, reasonPhrase[, headers])
|
|
response.statusMessage = reason;
|
|
} else {
|
|
// writeHead(statusCode[, headers])
|
|
if (!response.statusMessage) response.statusMessage = STATUS_CODES[statusCode] || "unknown";
|
|
obj ??= reason;
|
|
}
|
|
if (checkInvalidHeaderChar(response.statusMessage)) throw $ERR_INVALID_CHAR("statusMessage");
|
|
|
|
response.statusCode = statusCode;
|
|
|
|
{
|
|
// Slow-case: when progressive API and header fields are passed.
|
|
let k;
|
|
|
|
if ($isArray(obj)) {
|
|
const length = obj.length;
|
|
// Append all the headers provided in the array:
|
|
if (length && $isArray(obj[0])) {
|
|
for (let i = 0; i < length; i++) {
|
|
const k = obj[i];
|
|
if (k) response.appendHeader(k[0], k[1]);
|
|
}
|
|
} else {
|
|
if (length % 2 !== 0) {
|
|
throw $ERR_INVALID_ARG_VALUE("headers", obj);
|
|
}
|
|
// Test non-chunked message does not have trailer header set,
|
|
// message will be terminated by the first empty line after the
|
|
// header fields, regardless of the header fields present in the
|
|
// message, and thus cannot contain a message body or 'trailers'.
|
|
if (
|
|
(response.chunkedEncoding !== true || response.hasHeader("content-length")) &&
|
|
(response._trailer || response.hasHeader("trailer"))
|
|
) {
|
|
throw $ERR_HTTP_TRAILER_INVALID("Trailers are invalid with this transfer encoding");
|
|
}
|
|
// Headers in obj should override previous headers but still
|
|
// allow explicit duplicates. To do so, we first remove any
|
|
// existing conflicts, then use appendHeader.
|
|
|
|
for (let n = 0; n < length; n += 2) {
|
|
k = obj[n + 0];
|
|
response.removeHeader(k);
|
|
}
|
|
|
|
for (let n = 0; n < length; n += 2) {
|
|
k = obj[n];
|
|
if (k) response.appendHeader(k, obj[n + 1]);
|
|
}
|
|
}
|
|
} else if (obj) {
|
|
const keys = Object.keys(obj);
|
|
const length = keys.length;
|
|
// Retain for(;;) loop for performance reasons
|
|
// Refs: https://github.com/nodejs/node/pull/30958
|
|
for (let i = 0; i < length; i++) {
|
|
k = keys[i];
|
|
if (k) response.setHeader(k, obj[k]);
|
|
}
|
|
}
|
|
if (
|
|
(response.chunkedEncoding !== true || response.hasHeader("content-length")) &&
|
|
(response._trailer || response.hasHeader("trailer"))
|
|
) {
|
|
// remove the invalid content-length or trailer header
|
|
if (hasContentLength) {
|
|
response.removeHeader("trailer");
|
|
} else {
|
|
response.removeHeader("content-length");
|
|
}
|
|
throw $ERR_HTTP_TRAILER_INVALID("Trailers are invalid with this transfer encoding");
|
|
}
|
|
}
|
|
|
|
updateHasBody(response, statusCode);
|
|
}
|
|
|
|
Object.defineProperty(NodeHTTPServerSocket, "name", { value: "Socket" });
|
|
|
|
function ServerResponse(req, options): void {
|
|
if (!(this instanceof ServerResponse)) return new ServerResponse(req, options);
|
|
OutgoingMessage.$call(this, options);
|
|
|
|
this.useChunkedEncodingByDefault = true;
|
|
|
|
if ((this[kDeprecatedReplySymbol] = options?.[kDeprecatedReplySymbol])) {
|
|
this[controllerSymbol] = undefined;
|
|
this[firstWriteSymbol] = undefined;
|
|
this[deferredSymbol] = undefined;
|
|
this.write = ServerResponse_writeDeprecated;
|
|
this.end = ServerResponse_finalDeprecated;
|
|
}
|
|
|
|
this.req = req;
|
|
this.sendDate = true;
|
|
this._sent100 = false;
|
|
this[headerStateSymbol] = NodeHTTPHeaderState.none;
|
|
this[kPendingCallbacks] = [];
|
|
this.finished = false;
|
|
|
|
// this is matching node's behaviour
|
|
// https://github.com/nodejs/node/blob/cf8c6994e0f764af02da4fa70bc5962142181bf3/lib/_http_server.js#L192
|
|
if (req.method === "HEAD") this._hasBody = false;
|
|
|
|
if (options) {
|
|
const handle = options[kHandle];
|
|
|
|
if (handle) {
|
|
this[kHandle] = handle;
|
|
} else {
|
|
this[kHandle] = req[kHandle];
|
|
}
|
|
this[kRejectNonStandardBodyWrites] = options[kRejectNonStandardBodyWrites] ?? false;
|
|
} else {
|
|
this[kHandle] = req[kHandle];
|
|
}
|
|
|
|
this.statusCode = 200;
|
|
this.statusMessage = undefined;
|
|
this.chunkedEncoding = false;
|
|
}
|
|
$toClass(ServerResponse, "ServerResponse", OutgoingMessage);
|
|
|
|
ServerResponse.prototype._removedConnection = false;
|
|
|
|
ServerResponse.prototype._removedContLen = false;
|
|
|
|
ServerResponse.prototype._hasBody = true;
|
|
|
|
ServerResponse.prototype._ended = false;
|
|
|
|
ServerResponse.prototype[kRejectNonStandardBodyWrites] = undefined;
|
|
|
|
Object.defineProperty(ServerResponse.prototype, "headersSent", {
|
|
get() {
|
|
return (
|
|
this[headerStateSymbol] === NodeHTTPHeaderState.sent || this[headerStateSymbol] === NodeHTTPHeaderState.assigned
|
|
);
|
|
},
|
|
set(value) {
|
|
this[headerStateSymbol] = value ? NodeHTTPHeaderState.sent : NodeHTTPHeaderState.none;
|
|
},
|
|
});
|
|
|
|
ServerResponse.prototype._writeRaw = function (chunk, encoding, callback) {
|
|
return this.socket.write(chunk, encoding, callback);
|
|
};
|
|
|
|
ServerResponse.prototype.writeEarlyHints = function (hints, cb) {
|
|
let head = "HTTP/1.1 103 Early Hints\r\n";
|
|
|
|
validateObject(hints, "hints");
|
|
|
|
if (hints.link === null || hints.link === undefined) {
|
|
return;
|
|
}
|
|
|
|
const link = validateLinkHeaderValue(hints.link);
|
|
|
|
if (link.length === 0) {
|
|
return;
|
|
}
|
|
|
|
head += "Link: " + link + "\r\n";
|
|
|
|
for (const key of ObjectKeys(hints)) {
|
|
if (key !== "link") {
|
|
head += key + ": " + hints[key] + "\r\n";
|
|
}
|
|
}
|
|
|
|
head += "\r\n";
|
|
|
|
this._writeRaw(head, "ascii", cb);
|
|
};
|
|
|
|
ServerResponse.prototype.writeProcessing = function (cb) {
|
|
this._writeRaw("HTTP/1.1 102 Processing\r\n\r\n", "ascii", cb);
|
|
};
|
|
|
|
ServerResponse.prototype.writeContinue = function (cb) {
|
|
this.socket[kHandle]?.response?.writeContinue();
|
|
cb?.();
|
|
};
|
|
|
|
// This end method is actually on the OutgoingMessage prototype in Node.js
|
|
// But we don't want it for the fetch() response version.
|
|
ServerResponse.prototype.end = function (chunk, encoding, callback) {
|
|
const handle = this[kHandle];
|
|
if (handle?.aborted) {
|
|
return this;
|
|
}
|
|
|
|
if ($isCallable(chunk)) {
|
|
callback = chunk;
|
|
chunk = undefined;
|
|
encoding = undefined;
|
|
} else if ($isCallable(encoding)) {
|
|
callback = encoding;
|
|
encoding = undefined;
|
|
} else if (!$isCallable(callback)) {
|
|
callback = undefined;
|
|
}
|
|
|
|
if (hasServerResponseFinished(this, chunk, callback)) {
|
|
return this;
|
|
}
|
|
|
|
if (chunk && !this._hasBody) {
|
|
if (this[kRejectNonStandardBodyWrites]) {
|
|
throw $ERR_HTTP_BODY_NOT_ALLOWED();
|
|
} else {
|
|
// node.js just ignores the write in this case
|
|
chunk = undefined;
|
|
}
|
|
}
|
|
|
|
if (!handle) {
|
|
if ($isCallable(callback)) {
|
|
process.nextTick(callback);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
const headerState = this[headerStateSymbol];
|
|
callWriteHeadIfObservable(this, headerState);
|
|
|
|
const flags = handle.flags;
|
|
if (!!(flags & NodeHTTPResponseFlags.closed_or_completed)) {
|
|
// node.js will return true if the handle is closed but the internal state is not
|
|
// and will not throw or emit an error
|
|
return true;
|
|
}
|
|
if (headerState !== NodeHTTPHeaderState.sent) {
|
|
handle.cork(() => {
|
|
handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]);
|
|
|
|
// If handle.writeHead throws, we don't want headersSent to be set to true.
|
|
// So we set it here.
|
|
this[headerStateSymbol] = NodeHTTPHeaderState.sent;
|
|
|
|
// https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/_http_outgoing.js#L987
|
|
this._contentLength = handle.end(chunk, encoding, undefined, strictContentLength(this));
|
|
});
|
|
} else {
|
|
// If there's no data but you already called end, then you're done.
|
|
// We can ignore it in that case.
|
|
if (!(!chunk && handle.ended) && !handle.aborted) {
|
|
handle.end(chunk, encoding, undefined, strictContentLength(this));
|
|
}
|
|
}
|
|
this._header = " ";
|
|
const req = this.req;
|
|
const socket = req.socket;
|
|
if (!req._consuming && !req?._readableState?.resumeScheduled) {
|
|
req._dump();
|
|
}
|
|
this.detachSocket(socket);
|
|
this.finished = true;
|
|
process.nextTick(self => {
|
|
self._ended = true;
|
|
}, this);
|
|
this.emit("prefinish");
|
|
this._callPendingCallbacks();
|
|
|
|
if (callback) {
|
|
process.nextTick(
|
|
function (callback, self) {
|
|
// In Node.js, the "finish" event triggers the "close" event.
|
|
// So it shouldn't become closed === true until after "finish" is emitted and the callback is called.
|
|
self.emit("finish");
|
|
try {
|
|
callback();
|
|
} catch (err) {
|
|
self.emit("error", err);
|
|
}
|
|
|
|
process.nextTick(emitCloseNT, self);
|
|
},
|
|
callback,
|
|
this,
|
|
);
|
|
} else {
|
|
process.nextTick(function (self) {
|
|
self.emit("finish");
|
|
process.nextTick(emitCloseNT, self);
|
|
}, this);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
Object.defineProperty(ServerResponse.prototype, "writable", {
|
|
get() {
|
|
return !this._ended || !hasServerResponseFinished(this);
|
|
},
|
|
});
|
|
|
|
ServerResponse.prototype.write = function (chunk, encoding, callback) {
|
|
const handle = this[kHandle];
|
|
|
|
if ($isCallable(chunk)) {
|
|
callback = chunk;
|
|
chunk = undefined;
|
|
encoding = undefined;
|
|
} else if ($isCallable(encoding)) {
|
|
callback = encoding;
|
|
encoding = undefined;
|
|
} else if (!$isCallable(callback)) {
|
|
callback = undefined;
|
|
}
|
|
|
|
if (hasServerResponseFinished(this, chunk, callback)) {
|
|
return false;
|
|
}
|
|
if (chunk && !this._hasBody) {
|
|
if (this[kRejectNonStandardBodyWrites]) {
|
|
throw $ERR_HTTP_BODY_NOT_ALLOWED();
|
|
} else {
|
|
// node.js just ignores the write in this case
|
|
chunk = undefined;
|
|
}
|
|
}
|
|
let result = 0;
|
|
|
|
const headerState = this[headerStateSymbol];
|
|
callWriteHeadIfObservable(this, headerState);
|
|
|
|
if (!handle) {
|
|
if (this.socket) {
|
|
return this.socket.write(chunk, encoding, callback);
|
|
} else {
|
|
return OutgoingMessagePrototype.write.$call(this, chunk, encoding, callback);
|
|
}
|
|
}
|
|
|
|
const flags = handle.flags;
|
|
if (!!(flags & NodeHTTPResponseFlags.closed_or_completed)) {
|
|
// node.js will return true if the handle is closed but the internal state is not
|
|
// and will not throw or emit an error
|
|
return true;
|
|
}
|
|
|
|
if (this[headerStateSymbol] !== NodeHTTPHeaderState.sent) {
|
|
handle.cork(() => {
|
|
handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]);
|
|
|
|
// If handle.writeHead throws, we don't want headersSent to be set to true.
|
|
// So we set it here.
|
|
this[headerStateSymbol] = NodeHTTPHeaderState.sent;
|
|
result = handle.write(chunk, encoding, allowWritesToContinue.bind(this), strictContentLength(this));
|
|
});
|
|
} else {
|
|
result = handle.write(chunk, encoding, allowWritesToContinue.bind(this), strictContentLength(this));
|
|
}
|
|
|
|
if (result < 0) {
|
|
if (callback) {
|
|
// The write was buffered due to backpressure.
|
|
// We need to defer the callback until the write actually goes through.
|
|
this[kPendingCallbacks].push(callback);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
this._callPendingCallbacks();
|
|
if (callback) {
|
|
process.nextTick(callback);
|
|
}
|
|
this.emit("drain");
|
|
|
|
return true;
|
|
};
|
|
|
|
ServerResponse.prototype._callPendingCallbacks = function () {
|
|
const originalLength = this[kPendingCallbacks].length;
|
|
|
|
for (let i = 0; i < originalLength; ++i) {
|
|
process.nextTick(this[kPendingCallbacks][i]);
|
|
}
|
|
|
|
if (this[kPendingCallbacks].length == originalLength) {
|
|
// If the array wasn't somehow appended to, just set it to an empty array
|
|
this[kPendingCallbacks] = [];
|
|
} else {
|
|
// Otherwise, splice it.
|
|
this[kPendingCallbacks].splice(0, originalLength);
|
|
}
|
|
};
|
|
|
|
ServerResponse.prototype._finish = function () {
|
|
this.emit("prefinish");
|
|
};
|
|
|
|
ServerResponse.prototype.detachSocket = function (socket) {
|
|
if (socket._httpMessage === this) {
|
|
if (socket[kCloseCallback]) socket[kCloseCallback] = undefined;
|
|
socket.removeListener("close", onServerResponseClose);
|
|
socket._httpMessage = null;
|
|
}
|
|
|
|
this.socket = null;
|
|
};
|
|
|
|
ServerResponse.prototype._implicitHeader = function () {
|
|
if (this.headersSent) return;
|
|
// @ts-ignore
|
|
this.writeHead(this.statusCode);
|
|
};
|
|
|
|
Object.defineProperty(ServerResponse.prototype, "writableNeedDrain", {
|
|
get() {
|
|
return !this.destroyed && !this.finished && (this[kHandle]?.bufferedAmount ?? 1) !== 0;
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(ServerResponse.prototype, "writableFinished", {
|
|
get() {
|
|
return !!(this.finished && (!this[kHandle] || this[kHandle].finished));
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(ServerResponse.prototype, "writableLength", {
|
|
get() {
|
|
return this.writableFinished ? 0 : (this[kHandle]?.bufferedAmount ?? 0);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(ServerResponse.prototype, "writableHighWaterMark", {
|
|
get() {
|
|
return 64 * 1024;
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(ServerResponse.prototype, "closed", {
|
|
get() {
|
|
return this._closed;
|
|
},
|
|
});
|
|
|
|
ServerResponse.prototype._send = function (data, encoding, callback, _byteLength) {
|
|
const handle = this[kHandle];
|
|
if (!handle) {
|
|
return OutgoingMessagePrototype._send.$apply(this, arguments);
|
|
}
|
|
|
|
if (this[headerStateSymbol] !== NodeHTTPHeaderState.sent) {
|
|
handle.cork(() => {
|
|
handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]);
|
|
this[headerStateSymbol] = NodeHTTPHeaderState.sent;
|
|
handle.write(data, encoding, callback, strictContentLength(this));
|
|
});
|
|
} else {
|
|
handle.write(data, encoding, callback, strictContentLength(this));
|
|
}
|
|
};
|
|
|
|
ServerResponse.prototype.writeHead = function (statusCode, statusMessage, headers) {
|
|
if (this.headersSent) {
|
|
throw $ERR_HTTP_HEADERS_SENT("writeHead");
|
|
}
|
|
_writeHead(statusCode, statusMessage, headers, this);
|
|
|
|
this[headerStateSymbol] = NodeHTTPHeaderState.assigned;
|
|
|
|
return this;
|
|
};
|
|
|
|
ServerResponse.prototype.assignSocket = function (socket) {
|
|
if (socket._httpMessage) {
|
|
throw $ERR_HTTP_SOCKET_ASSIGNED("Socket already assigned");
|
|
}
|
|
socket._httpMessage = this;
|
|
socket.once("close", onServerResponseClose);
|
|
this.socket = socket;
|
|
this.emit("socket", socket);
|
|
};
|
|
|
|
Object.defineProperty(ServerResponse.prototype, "shouldKeepAlive", {
|
|
get() {
|
|
return this[kHandle]?.shouldKeepAlive ?? true;
|
|
},
|
|
set(_value) {
|
|
// throw new Error('not implemented');
|
|
},
|
|
});
|
|
|
|
ServerResponse.prototype.destroy = function (_err?: Error) {
|
|
if (this.destroyed) return this;
|
|
const handle = this[kHandle];
|
|
this.destroyed = true;
|
|
if (handle) {
|
|
handle.abort();
|
|
}
|
|
this?.socket?.destroy();
|
|
this.emit("close");
|
|
return this;
|
|
};
|
|
|
|
ServerResponse.prototype.emit = function (event) {
|
|
if (event === "close") {
|
|
callCloseCallback(this);
|
|
}
|
|
return Stream.prototype.emit.$apply(this, arguments);
|
|
};
|
|
|
|
ServerResponse.prototype.flushHeaders = function () {
|
|
if (this[headerStateSymbol] === NodeHTTPHeaderState.sent) return; // Should be idempotent.
|
|
if (this[headerStateSymbol] !== NodeHTTPHeaderState.assigned) this._implicitHeader();
|
|
|
|
const handle = this[kHandle];
|
|
if (handle) {
|
|
if (this[headerStateSymbol] === NodeHTTPHeaderState.assigned) {
|
|
this[headerStateSymbol] = NodeHTTPHeaderState.sent;
|
|
|
|
handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]);
|
|
}
|
|
handle.flushHeaders();
|
|
}
|
|
};
|
|
|
|
function updateHasBody(response, statusCode) {
|
|
// RFC 2616, 10.2.5:
|
|
// The 204 response MUST NOT include a message-body, and thus is always
|
|
// terminated by the first empty line after the header fields.
|
|
// RFC 2616, 10.3.5:
|
|
// The 304 response MUST NOT contain a message-body, and thus is always
|
|
// terminated by the first empty line after the header fields.
|
|
// RFC 2616, 10.1 Informational 1xx:
|
|
// This class of status code indicates a provisional response,
|
|
// consisting only of the Status-Line and optional headers, and is
|
|
// terminated by an empty line.
|
|
if (statusCode === 204 || statusCode === 304 || (statusCode >= 100 && statusCode <= 199)) {
|
|
response._hasBody = false;
|
|
} else {
|
|
response._hasBody = true;
|
|
}
|
|
}
|
|
|
|
function emitServerSocketEOF(self, req) {
|
|
self.push(null);
|
|
if (req) {
|
|
req.push(null);
|
|
req.complete = true;
|
|
}
|
|
}
|
|
|
|
function emitServerSocketEOFNT(self, req) {
|
|
if (req) {
|
|
req[eofInProgress] = true;
|
|
}
|
|
process.nextTick(emitServerSocketEOF, self);
|
|
}
|
|
|
|
let OriginalWriteHeadFn, OriginalImplicitHeadFn;
|
|
|
|
function callWriteHeadIfObservable(self, headerState) {
|
|
if (
|
|
headerState === NodeHTTPHeaderState.none &&
|
|
!(self.writeHead === OriginalWriteHeadFn && self._implicitHeader === OriginalImplicitHeadFn)
|
|
) {
|
|
self.writeHead(self.statusCode, self.statusMessage, self[headersSymbol]);
|
|
}
|
|
}
|
|
|
|
function allowWritesToContinue() {
|
|
this._callPendingCallbacks();
|
|
this.emit("drain");
|
|
}
|
|
|
|
function drainHeadersIfObservable() {
|
|
if (this._implicitHeader === OriginalImplicitHeadFn && this.writeHead === OriginalWriteHeadFn) {
|
|
return;
|
|
}
|
|
|
|
this._implicitHeader();
|
|
}
|
|
|
|
function ServerResponse_finalDeprecated(chunk, encoding, callback) {
|
|
if ($isCallable(encoding)) {
|
|
callback = encoding;
|
|
encoding = undefined;
|
|
}
|
|
if (!$isCallable(callback)) {
|
|
callback = undefined;
|
|
}
|
|
|
|
if (this.destroyed || this.finished) {
|
|
if (chunk) {
|
|
emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_WRITE_AFTER_END(), callback);
|
|
}
|
|
return false;
|
|
}
|
|
if (encoding && encoding !== "buffer") {
|
|
chunk = Buffer.from(chunk, encoding);
|
|
}
|
|
const req = this.req;
|
|
|
|
const shouldEmitClose = req && req.emit && !this.finished;
|
|
if (!this.headersSent) {
|
|
let data = this[firstWriteSymbol];
|
|
if (chunk) {
|
|
if (data) {
|
|
if (encoding) {
|
|
data = Buffer.from(data, encoding);
|
|
}
|
|
|
|
data = new Blob([data, chunk]);
|
|
} else {
|
|
data = chunk;
|
|
}
|
|
} else if (!data) {
|
|
data = undefined;
|
|
} else {
|
|
data = new Blob([data]);
|
|
}
|
|
|
|
this[firstWriteSymbol] = undefined;
|
|
this.finished = true;
|
|
this.headersSent = true; // https://github.com/oven-sh/bun/issues/3458
|
|
drainHeadersIfObservable.$call(this);
|
|
this[kDeprecatedReplySymbol](
|
|
new Response(data, {
|
|
headers: this[headersSymbol],
|
|
status: this.statusCode,
|
|
statusText: this.statusMessage ?? STATUS_CODES[this.statusCode],
|
|
}),
|
|
);
|
|
if (shouldEmitClose) {
|
|
req.complete = true;
|
|
process.nextTick(emitRequestCloseNT, req);
|
|
}
|
|
callback?.();
|
|
return;
|
|
}
|
|
|
|
this.finished = true;
|
|
ensureReadableStreamController.$call(this, controller => {
|
|
if (chunk && encoding) {
|
|
chunk = Buffer.from(chunk, encoding);
|
|
}
|
|
|
|
let prom;
|
|
if (chunk) {
|
|
controller.write(chunk);
|
|
prom = controller.end();
|
|
} else {
|
|
prom = controller.end();
|
|
}
|
|
|
|
const handler = () => {
|
|
callback();
|
|
const deferred = this[deferredSymbol];
|
|
if (deferred) {
|
|
this[deferredSymbol] = undefined;
|
|
deferred();
|
|
}
|
|
};
|
|
if ($isPromise(prom)) prom.then(handler, handler);
|
|
else handler();
|
|
});
|
|
}
|
|
|
|
// ServerResponse.prototype._final = ServerResponse_finalDeprecated;
|
|
|
|
ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead;
|
|
|
|
OriginalWriteHeadFn = ServerResponse.prototype.writeHead;
|
|
OriginalImplicitHeadFn = ServerResponse.prototype._implicitHeader;
|
|
|
|
function storeHTTPOptions(options) {
|
|
this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
|
|
this[kServerResponse] = options.ServerResponse || ServerResponse;
|
|
|
|
const maxHeaderSize = options.maxHeaderSize;
|
|
if (maxHeaderSize !== undefined) validateInteger(maxHeaderSize, "maxHeaderSize", 0);
|
|
this.maxHeaderSize = maxHeaderSize;
|
|
|
|
const insecureHTTPParser = options.insecureHTTPParser;
|
|
if (insecureHTTPParser !== undefined) validateBoolean(insecureHTTPParser, "options.insecureHTTPParser");
|
|
this.insecureHTTPParser = insecureHTTPParser;
|
|
|
|
const requestTimeout = options.requestTimeout;
|
|
if (requestTimeout !== undefined) {
|
|
validateInteger(requestTimeout, "requestTimeout", 0);
|
|
this.requestTimeout = requestTimeout;
|
|
} else {
|
|
this.requestTimeout = 300_000; // 5 minutes
|
|
}
|
|
|
|
const headersTimeout = options.headersTimeout;
|
|
if (headersTimeout !== undefined) {
|
|
validateInteger(headersTimeout, "headersTimeout", 0);
|
|
this.headersTimeout = headersTimeout;
|
|
} else {
|
|
this.headersTimeout = MathMin(60_000, this.requestTimeout); // Minimum between 60 seconds or requestTimeout
|
|
}
|
|
|
|
if (this.requestTimeout > 0 && this.headersTimeout > 0 && this.headersTimeout > this.requestTimeout) {
|
|
throw $ERR_OUT_OF_RANGE("headersTimeout", "<= requestTimeout", headersTimeout);
|
|
}
|
|
|
|
const keepAliveTimeout = options.keepAliveTimeout;
|
|
if (keepAliveTimeout !== undefined) {
|
|
validateInteger(keepAliveTimeout, "keepAliveTimeout", 0);
|
|
this.keepAliveTimeout = keepAliveTimeout;
|
|
} else {
|
|
this.keepAliveTimeout = 5_000; // 5 seconds;
|
|
}
|
|
|
|
const connectionsCheckingInterval = options.connectionsCheckingInterval;
|
|
if (connectionsCheckingInterval !== undefined) {
|
|
validateInteger(connectionsCheckingInterval, "connectionsCheckingInterval", 0);
|
|
this.connectionsCheckingInterval = connectionsCheckingInterval;
|
|
} else {
|
|
this.connectionsCheckingInterval = 30_000; // 30 seconds
|
|
}
|
|
|
|
const requireHostHeader = options.requireHostHeader;
|
|
if (requireHostHeader !== undefined) {
|
|
validateBoolean(requireHostHeader, "options.requireHostHeader");
|
|
this.requireHostHeader = requireHostHeader;
|
|
} else {
|
|
this.requireHostHeader = true;
|
|
}
|
|
|
|
const joinDuplicateHeaders = options.joinDuplicateHeaders;
|
|
if (joinDuplicateHeaders !== undefined) {
|
|
validateBoolean(joinDuplicateHeaders, "options.joinDuplicateHeaders");
|
|
}
|
|
this.joinDuplicateHeaders = joinDuplicateHeaders;
|
|
|
|
const rejectNonStandardBodyWrites = options.rejectNonStandardBodyWrites;
|
|
if (rejectNonStandardBodyWrites !== undefined) {
|
|
validateBoolean(rejectNonStandardBodyWrites, "options.rejectNonStandardBodyWrites");
|
|
this.rejectNonStandardBodyWrites = rejectNonStandardBodyWrites;
|
|
} else {
|
|
this.rejectNonStandardBodyWrites = false;
|
|
}
|
|
}
|
|
|
|
function ensureReadableStreamController(run) {
|
|
const thisController = this[controllerSymbol];
|
|
if (thisController) return run(thisController);
|
|
this.headersSent = true;
|
|
let firstWrite = this[firstWriteSymbol];
|
|
const old_run = this[runSymbol];
|
|
if (old_run) {
|
|
old_run.push(run);
|
|
return;
|
|
}
|
|
this[runSymbol] = [run];
|
|
this[kDeprecatedReplySymbol](
|
|
new Response(
|
|
new ReadableStream({
|
|
type: "direct",
|
|
pull: controller => {
|
|
this[controllerSymbol] = controller;
|
|
if (firstWrite) controller.write(firstWrite);
|
|
firstWrite = undefined;
|
|
for (let run of this[runSymbol]) {
|
|
run(controller);
|
|
}
|
|
if (!this.finished) {
|
|
const { promise, resolve } = $newPromiseCapability(GlobalPromise);
|
|
this[deferredSymbol] = resolve;
|
|
return promise;
|
|
}
|
|
},
|
|
}),
|
|
{
|
|
headers: this[headersSymbol],
|
|
status: this.statusCode,
|
|
statusText: this.statusMessage ?? STATUS_CODES[this.statusCode],
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
export default {
|
|
Server,
|
|
ServerResponse,
|
|
kConnectionsCheckingInterval,
|
|
};
|