Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ We update this page regularly. It reflects the latest version of Bun's compatibi

### [`node:http`](https://nodejs.org/api/http.html)

🟢 Fully implemented. `http.Server` does not extend `net.Server`. Bun ignores `listen(handle)` and the `fd`, `ipv6Only` and `signal` options of `listen()`. `keepAlive`/`keepAliveInitialDelay` on the server are no-ops.
🟢 Fully implemented. `http.Server` does not extend `net.Server`. Bun ignores `listen(handle)` and the `fd`, `ipv6Only` and `signal` options of `listen()`. The sockets of a server always have `TCP_NODELAY` on: the `noDelay` option and `socket.setNoDelay(false)` have no effect.

### [`node:https`](https://nodejs.org/api/https.html)

🟡 `request`, `get`, `Agent` and `globalAgent` are implemented, including connection pooling. Client sockets are `tls.TLSSocket`s. `https.Server` is `http.Server` with TLS options rather than a `tls.Server`. Its request sockets (`req.socket`) are not `tls.TLSSocket`s: `encrypted`, `authorized` and `servername` work, but `getPeerCertificate()` and `getCipher()` are missing. `setSecureContext()`, `addContext()`, `SNICallback` and `handshakeTimeout` are not supported.
🟡 `request`, `get`, `Agent` and `globalAgent` are implemented, including connection pooling. Client sockets are `tls.TLSSocket`s. `https.Server` is `http.Server` with TLS options rather than a `tls.Server`. Its request sockets (`req.socket`) are not `tls.TLSSocket`s: `encrypted`, `authorized` and `servername` work, but `getPeerCertificate()` and `getCipher()` are missing. `setSecureContext()`, `addContext()`, `SNICallback` and `handshakeTimeout` are not supported. The `keepAlive` option takes effect when the TLS handshake completes, not when the connection is accepted.

### [`node:os`](https://nodejs.org/api/os.html)

Expand Down
5 changes: 5 additions & 0 deletions src/js/internal/net/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export default {
// Internal handshake-settled signal: server-side sockets emit no user
// 'secureConnect' (node parity), so internal deferrals park on this instead.
kSecureConnectDone: Symbol("kSecureConnectDone"),
// What net.Socket#setKeepAlive() last gave to the handle. The delay is in milliseconds.
kSetKeepAlive: Symbol("kSetKeepAlive"),
kSetKeepAliveInitialDelay: Symbol("kSetKeepAliveInitialDelay"),
// On a net.Socket subclass with no `_handle` (a node:http server socket): the handle that setKeepAlive() acts on.
kSocketOptionHandle: Symbol("kSocketOptionHandle"),
kVerifyError: Symbol("kVerifyError"),
// net.Socket.prototype method behind the client-side `new tls.TLSSocket(socket)`.
kUpgradeClientTLS: Symbol("kUpgradeClientTLS"),
Expand Down
39 changes: 37 additions & 2 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
validateLinkHeaderValue,
validateBoolean,
validateInteger,
validateNumber,
validateFunction,
validateOneOf,
} = require("internal/validators");
Expand All @@ -31,6 +32,7 @@ const kServerResponseStatistics = Symbol("ServerResponseStatistics");

const { isPrimary } = require("internal/cluster/isPrimary");
const { addServerAbortSignalOption } = require("internal/net/server_abort_signal");
const { kSetKeepAlive, kSetKeepAliveInitialDelay, kSocketOptionHandle } = require("internal/net/symbols");
const {
kInternalSocketData,
serverSymbol,
Expand Down Expand Up @@ -301,6 +303,8 @@ interface Server extends NodeHTTPServer {
requireHostHeader: boolean;
httpAllowHalfOpen: boolean;
blockList?: import("node:net").BlockList;
keepAlive: boolean;
keepAliveInitialDelay: number;
}
function Server(options, callback): void {
if (!(this instanceof Server)) return new Server(options, callback);
Expand Down Expand Up @@ -440,6 +444,15 @@ function Server(options, callback): void {
this[optionsSymbol] = options;
storeHTTPOptions.$call(this, options);

// Node's net.Server checks keepAliveInitialDelay and keeps it in seconds: https://github.com/nodejs/node/blob/v26.3.0/lib/net.js#L1850-L1858 and https://github.com/nodejs/node/blob/v26.3.0/lib/net.js#L1881-L1882
let keepAliveInitialDelay = options.keepAliveInitialDelay;
if (keepAliveInitialDelay !== undefined) {
validateNumber(keepAliveInitialDelay, "options.keepAliveInitialDelay");
if (keepAliveInitialDelay < 0) keepAliveInitialDelay = 0;
}
this.keepAlive = Boolean(options.keepAlive);
this.keepAliveInitialDelay = ~~(keepAliveInitialDelay / 1000);

if (callback) this.on("request", callback);
return this;
}
Expand Down Expand Up @@ -750,6 +763,7 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
) {
if (!socket) {
socket = new (getNodeHTTPServerSocket())(server, socketHandle, !!tls, listenerGeneration);
if (server.keepAlive) setKeepAliveOfServer(server, socket, !!tls);
}

if (isPipelinedDispatch) {
Expand Down Expand Up @@ -1240,6 +1254,21 @@ function defineMaxHeadersCount(server: Server) {
});
}

// Node's net.Server gives keep-alive to each connection it accepts, from the two properties as they are then: https://github.com/nodejs/node/blob/v26.3.0/lib/net.js#L2361-L2365
function setKeepAliveOfServer(server: Server, socket: NodeHTTPServerSocket, isTLS: boolean) {
const handle = socket[kHandle];
if (!handle.setKeepAlive) return;
// Node gives the seconds to the handle as an unsigned int. This handle takes milliseconds.
const seconds = server.keepAliveInitialDelay;
const delay = (typeof seconds === "number" ? seconds >>> 0 : 0) * 1000;
// Node's https req.socket is a TLSSocket, which starts with no keep-alive state. Its http socket starts from what the server set.
if (!isTLS) {
socket[kSetKeepAlive] = true;
socket[kSetKeepAliveInitialDelay] = delay;
}
handle.setKeepAlive(true, delay);
}

// Native callback fired when the server accepts a connection (for TLS, when
// its handshake completes), before any request bytes - like Node.js's
// net.Server 'connection' / tls.Server 'secureConnection' events.
Expand Down Expand Up @@ -1290,6 +1319,8 @@ function onServerConnection(this: Server, listenerGeneration, socketHandle) {
}
}

if (this.keepAlive) setKeepAliveOfServer(this, socket, isTLS);

// Node's connectionListener attaches the HTTPParser (socket.parser) before
// emitting 'connection'; expose the shim here so listeners see it populated.
socket.parser = createServerParserShim(socket);
Expand Down Expand Up @@ -1366,6 +1397,7 @@ function onServerClientError(
const existingDuplex = (socket as any).duplex;
const nodeSocket = existingDuplex ?? new (getNodeHTTPServerSocket())(self, socket, ssl, listenerGeneration);
if (!existingDuplex) {
if (self.keepAlive) setKeepAliveOfServer(self, nodeSocket, ssl);
nodeSocket.parser = createServerParserShim(nodeSocket);
self.emit("connection", nodeSocket);
}
Expand Down Expand Up @@ -1708,6 +1740,8 @@ function getNodeHTTPServerSocket() {
this._writableState.decodeStrings = true;
this.server = server;
this.#resetSupported = !encrypted && !listenerGeneration?.isUnix;
// A unix socket has no keep-alive: its handle gets no setKeepAlive(), as node's Pipe handle.
if (listenerGeneration?.isUnix) handle.setKeepAlive = undefined;
this[kHandle] = handle;
this._secureEstablished = !!handle?.secureEstablished;
handle.onclose = this.#onClose.bind(this);
Expand Down Expand Up @@ -2158,8 +2192,9 @@ function getNodeHTTPServerSocket() {
return this;
}

setKeepAlive(_enable = false, _initialDelay = 0) {
return this;
// What net.Socket#setKeepAlive() acts on.
get [kSocketOptionHandle]() {
return this[kHandle];
}

setNoDelay(_noDelay = true) {
Expand Down
12 changes: 7 additions & 5 deletions src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const {
kDestroyOnRead,
kPreHandshakeWrite,
kSecureConnectDone,
kSetKeepAlive,
kSetKeepAliveInitialDelay,
kSocketOptionHandle,
kStandaloneWrap,
kUpgradeClientTLS,
kVerifyError,
Expand Down Expand Up @@ -298,9 +301,7 @@ const kReinitializeHandle = Symbol("kReinitializeHandle");
const kRealListen = Symbol("kRealListen");
const kSetNoDelay = Symbol("kSetNoDelay");
const kSetTOS = Symbol("kSetTOS");
const kSetKeepAlive = Symbol("kSetKeepAlive");
const kSyncWriteFd = Symbol("kSyncWriteFd");
const kSetKeepAliveInitialDelay = Symbol("kSetKeepAliveInitialDelay");
const kConnectOptions = Symbol("connect-options");
const kAttach = Symbol("kAttach");
const kCloseRawConnection = Symbol("kCloseRawConnection");
Expand Down Expand Up @@ -2858,18 +2859,19 @@ Socket.prototype.setKeepAlive = function setKeepAlive(enable = false, initialDel
// negatives and ~~ overflow match Node's no-validate behavior.
const initialDelay = MathMax(0, ~~initialDelayMsecs);

if (!this._handle) {
const handle = this._handle || this[kSocketOptionHandle];
if (!handle) {
this[kSetKeepAlive] = enable;
this[kSetKeepAliveInitialDelay] = initialDelay;
return this;
}
if (!this._handle.setKeepAlive) {
if (!handle.setKeepAlive) {
return this;
}
if (enable !== this[kSetKeepAlive] || (enable && this[kSetKeepAliveInitialDelay] !== initialDelay)) {
this[kSetKeepAlive] = enable;
this[kSetKeepAliveInitialDelay] = initialDelay;
this._handle.setKeepAlive(enable, initialDelay);
handle.setKeepAlive(enable, initialDelay);
}
return this;
};
Expand Down
23 changes: 23 additions & 0 deletions src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
#include "helpers.h"
#include <JavaScriptCore/JSCJSValueInlines.h>
#include <wtf/text/WTFString.h>
#include <algorithm>
#include <cmath>
#include <limits>

extern "C" EncodedJSValue us_socket_buffered_js_write(void* socket, bool is_ssl, bool ended, bool hold, bool flushesBufferOnDrain, us_socket_stream_buffer_t* streamBuffer, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue data, JSC::EncodedJSValue encoding);
extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6);
extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6);
extern "C" void us_socket_shutdown(us_socket_t*);
extern "C" int us_socket_keepalive(us_socket_t*, int enabled, unsigned int delay);
extern "C" JSC::EncodedJSValue Bun__socketReadErrorFromCloseCode(JSC::JSGlobalObject* globalObject, int code);

namespace Bun {
Expand Down Expand Up @@ -52,6 +55,7 @@ JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketStartPipelinedResponse);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketStopParsing);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketCloseWhenDrained);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketCloseIfIdle);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketSetKeepAlive);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress);
Expand Down Expand Up @@ -97,6 +101,7 @@ static const JSC::HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] =
{ "stopParsing"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketStopParsing, 0 } },
{ "closeWhenDrained"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketCloseWhenDrained, 0 } },
{ "closeIfIdle"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketCloseIfIdle, 0 } },
{ "setKeepAlive"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketSetKeepAlive, 2 } },
{ "secureEstablished"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterIsSecureEstablished, noOpSetter } },
{ "servername"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterServername, noOpSetter } },
{ "authorizationError"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterAuthorizationError, noOpSetter } },
Expand Down Expand Up @@ -264,6 +269,24 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketCloseIfIdle, (JSC::JSGlob
return JSValue::encode(JSC::jsBoolean(thisObject && thisObject->closeIfIdle()));
}

// The delay is in milliseconds, as for the handle of Bun.Socket. No `upgraded` check: the connection of a WebSocket keeps its keep-alive, as in Node.
JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketSetKeepAlive, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
bool enabled = callFrame->argument(0).toBoolean(globalObject);
double delayMs = callFrame->argument(1).toNumber(globalObject);
RETURN_IF_EXCEPTION(scope, {});
// After toNumber(): it can run JS that closes the connection.
auto* thisObject = dynamicDowncast<JSNodeHTTPServerSocket>(callFrame->thisValue());
if (!thisObject || thisObject->isClosed()) [[unlikely]] {
return JSValue::encode(JSC::jsBoolean(false));
}
// TCP_KEEPIDLE is in seconds. 0, also for NaN and a negative delay, keeps the kernel's value.
unsigned delaySeconds = delayMs >= 1000 ? static_cast<unsigned>(std::min(delayMs / 1000, static_cast<double>(std::numeric_limits<unsigned>::max()))) : 0;
return JSValue::encode(JSC::jsBoolean(us_socket_keepalive(thisObject->socket, enabled, delaySeconds) == 0));
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto* thisObject = dynamicDowncast<JSNodeHTTPServerSocket>(callFrame->thisValue());
Expand Down
Loading
Loading