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
17 changes: 16 additions & 1 deletion src/js/node/_http_incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
noBodySymbol,
emitErrorNextTickIfErrorListenerNT,
NodeHTTPBodyReadState,
NodeHTTPResponseAbortEvent,
emitEOFIncomingMessage,
onDataIncomingMessage,
kAbortController,
Expand Down Expand Up @@ -366,6 +367,15 @@ IncomingMessage.prototype._read = function _read(_n) {
}
};

// Like Node's parserOnMessageComplete: the connection outlives this request, and the last chunk of its body completes the message and brings the trailers.
function onDataDestroyedIncomingMessage(this: any, handle, _chunk, isLast, event) {
if (!isLast || event !== NodeHTTPResponseAbortEvent.none) return;
this.complete = true;
// The stream destroyer cleared req.socket, and req.client is the same connection.
const rawTrailers = handle.takeRequestTrailers(this.client?.server?.insecureHTTPParser === true);
Comment thread
robobun marked this conversation as resolved.
Resolved
if (rawTrailers !== undefined) this._addHeaderLines(rawTrailers, rawTrailers.length);
}
Comment thread
robobun marked this conversation as resolved.
Resolved

// It's possible that the socket will be destroyed, and removed from
// any messages, before ever calling this. In that case, just skip
// it, since something else is destroying this connection anyway.
Expand All @@ -384,7 +394,12 @@ IncomingMessage.prototype._destroy = function _destroy(err, cb) {
// connection must outlive the request so the response can still reply
// (node's _destroy null-socket check).
this[kHandle] = undefined;
handle.onabort = handle.ondata = undefined;
if (!this.socket && !this.complete) {
handle.ondata = onDataDestroyedIncomingMessage.bind(this, handle);
handle.onabort = undefined;
Comment thread
robobun marked this conversation as resolved.
Resolved
} else {
handle.onabort = handle.ondata = undefined;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if (!handle.finished && shouldEmitAborted && this.socket) {
handle.abort();
}
Expand Down
32 changes: 24 additions & 8 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ const { kIncomingMessage } = require("node:_http_common");
let http1Fallback;
const kConnectionsCheckingInterval = Symbol("http.server.connectionsCheckingInterval");
const kTrackedConnections = Symbol("http.server.trackedConnections");
// The http.Server that listens on a native server. Under bun --hot a native server outlives the http.Server of each run of the script.
const nativeServerOwners = new WeakMap<object, Server>();
const kPendingDrainClose = Symbol("http.server.pendingDrainClose");
const kPendingCloseGenerations = Symbol("http.server.pendingCloseGenerations");
const kListenerGeneration = Symbol("http.server.listenerGeneration");
Expand Down Expand Up @@ -537,16 +539,19 @@ Server.prototype.getConnections = function (callback) {

Server.prototype.closeIdleConnections = function () {
http1Fallback?.closeIdleHttp1Connections(this);
const server = this[serverSymbol];
if (server) {
server.closeIdleConnections();
return;
}
const tracked = this[kTrackedConnections];
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
if (tracked && tracked.size > 0) {
for (const socket of $Array.from(tracked) as NodeHTTPServerSocket[]) {
// uWS knows whether a request is arriving on the connection. A missing _httpMessage does not tell.
if (!socket[kHandedOff]) socket[kHandle]?.closeIfIdle();
// After close() the connection of an ended response goes at once, so that 'close' does not wait for its keepAliveTimeout.
const listening = !!this[serverSymbol];
const sockets = $Array.from(tracked) as NodeHTTPServerSocket[];
// The newest connection first, like the sweep of uWS.
for (let i = sockets.length - 1; i >= 0; i--) {
const socket = sockets[i];
if (socket[kHandedOff]) continue;
// Like Node's parser, busy until its message is complete, so also in the listener of a request without a body: https://github.com/nodejs/node/blob/v26.3.0/src/node_http_parser.cc#L1153
if (listening && socket.parser?.incoming?.complete === false) continue;
// uWS knows the rest: a request head or body that arrives, a response in flight. A missing _httpMessage does not tell.
socket[kHandle]?.closeIfIdle();
}
}
};
Expand Down Expand Up @@ -1157,6 +1162,17 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
});

const handle = this[serverSymbol];
const previousOwner = nativeServerOwners.get(handle);
if (previousOwner !== undefined && previousOwner !== this) {
// bun --hot: Bun.serve() gave back the native server of the last run of the script. Its connections are this server's now.
const connections = this[kTrackedConnections];
for (const socket of previousOwner[kTrackedConnections]) {
socket.server = this;
connections.add(socket);
}
previousOwner[kTrackedConnections].clear();
}
nativeServerOwners.set(handle, this);
listenerGeneration = {
isUnix: !!socketPath,
nativeClosed: false,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/NodeHTTPResponse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pub(crate) struct NodeHTTPResponse {
pub(crate) request_trailers: JsCell<Vec<u8>>,
/// The JS wrapper whose `ondata` slot was armed for THIS request body. `get_this_value()`
/// resolves through the socket's current response, which under pipelining is a different
/// one; delivering through it loses the body. Kept alive by req[kHandle]; cleared on finalize.
/// one; delivering through it loses the body. Rooted by the server socket; cleared on finalize.
pub(crate) armed_this_value: Cell<JSValue>,
/// node:http: this request's header section captured at dispatch as
/// [u32 nameLen][u32 valueLen][name][value]... so req.rawHeaders /
Expand Down
Loading
Loading