Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
13 changes: 13 additions & 0 deletions packages/bun-uws/src/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,19 @@ struct TemplatedApp {
return std::move(*this);
}

/* node:http only: enable per-socket headersTimeout/requestTimeout receive
* deadlines (seconds; 0 disables that phase's deadline). */
TemplatedApp &&setNodeReceiveTimeouts(unsigned int headersTimeoutSeconds, unsigned int requestTimeoutSeconds) {
/* us_socket_timeout's slot ring wraps past 240 4-second ticks; larger
* values would alias to a much earlier expiry. */
constexpr unsigned int maxTimeoutSeconds = 940;
HttpContextData<SSL> *data = httpContext->getSocketContextData();
data->nodeHeadersTimeoutSeconds = headersTimeoutSeconds < maxTimeoutSeconds ? headersTimeoutSeconds : maxTimeoutSeconds;
data->nodeRequestTimeoutSeconds = requestTimeoutSeconds < maxTimeoutSeconds ? requestTimeoutSeconds : maxTimeoutSeconds;
data->flags.hasNodeReceiveTimeouts = true;
return std::move(*this);
}

};

typedef TemplatedApp<false> App;
Expand Down
158 changes: 152 additions & 6 deletions packages/bun-uws/src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include <span>
#include <array>
#include <mutex>
#include <chrono>


namespace uWS {
Expand Down Expand Up @@ -124,6 +125,87 @@ struct HttpContext {
/* Minimum allowed receive throughput per second (clients uploading less than 16kB/sec get dropped) */
static constexpr int HTTP_RECEIVE_THROUGHPUT_BYTES = 16 * 1024;

public:
/* The configured node:http deadline (whole seconds) governing a receive
* phase, 0 = disabled. requestTimeout spans the whole message including
* its header section, so the headers phase uses the tighter of the two. */
static unsigned int nodeConfiguredReceiveSeconds(us_socket_t *s, NodeReceivePhase phase) {
HttpContextData<SSL> *httpContextData = getSocketContextDataS(s);
unsigned int headersSeconds = httpContextData->nodeHeadersTimeoutSeconds;
unsigned int requestSeconds = httpContextData->nodeRequestTimeoutSeconds;
if (phase == NodeReceivePhase::Headers && headersSeconds && (!requestSeconds || headersSeconds < requestSeconds)) {
return headersSeconds;
}
return requestSeconds;
}

/* See NodeReceivePhase in HttpParser.h. Returns None — meaning the legacy
* idle timeout owns the per-socket timer — both outside the receive phases
* and when the current phase's configured deadline is 0 (disabled). */
static NodeReceivePhase nodeReceivePhase(us_socket_t *s, HttpResponseData<SSL> *httpResponseData) {
NodeReceivePhase phase;
/* isConnectRequest is set as soon as the CONNECT request line parses;
* until its header section is complete (partial bytes buffered) it is
* still subject to headersTimeout. Only an established tunnel has no phase. */
if (httpResponseData->isConnectRequest && !httpResponseData->hasPartialRequest()) {
return NodeReceivePhase::None;
}
/* requestTimeout applies until the message is fully received, even if
* the handler already responded (Node keeps enforcing it on the
* outstanding body, then dumps it). */
if (httpResponseData->isReceivingHttpBody()) {
phase = NodeReceivePhase::Body;
} else if (httpResponseData->state & HttpResponseData<SSL>::HTTP_RESPONSE_PENDING) {
return NodeReceivePhase::None;
} else if (httpResponseData->hasPartialRequest() || !httpResponseData->hasCompletedResponse) {
phase = NodeReceivePhase::Headers;
} else {
return NodeReceivePhase::None;
}
/* A phase whose deadline is disabled never claims the timer: a user
* req.setTimeout()/server idle timeout must keep working through it. */
return nodeConfiguredReceiveSeconds(s, phase) ? phase : NodeReceivePhase::None;
}

static uint64_t nodeNowMs() {
return (uint64_t) std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
}

/* Seconds left until the current message's deadline, measured from the
* message start like Node: the headers deadline is additionally capped by
* requestTimeout (which spans the whole message including its header
* section), and the body deadline is whatever remains of requestTimeout.
* Only called for the phase nodeReceivePhase() returned, whose configured
* deadline is therefore non-zero. */
static unsigned int nodeRemainingReceiveSeconds(us_socket_t *s, HttpResponseData<SSL> *httpResponseData, NodeReceivePhase phase) {
unsigned int total = nodeConfiguredReceiveSeconds(s, phase);
uint64_t elapsedSeconds = (nodeNowMs() - httpResponseData->nodeMessageStartMs) / 1000;
/* Already past the deadline: fire at the next timer sweep */
return elapsedSeconds >= total ? 1 : (unsigned int) (total - elapsedSeconds);
}

/* While a receive phase is active its deadline owns the per-socket timer;
* every legacy resetTimeout()/pause() routes back through here so it
* cannot be disarmed early. Returns false outside the receive phases, in
* which case the caller falls back to the legacy idle timeout. */
static bool tryArmNodeReceiveTimeout(us_socket_t *s, HttpResponseData<SSL> *httpResponseData) {
NodeReceivePhase phase = nodeReceivePhase(s, httpResponseData);
if (phase == NodeReceivePhase::None) {
return false;
}
/* Like Node (last_message_start_), the clock is the message's first
* received byte (or connection open before any byte arrives). onData
* and the parser own nodeMessageStartMs; this only reads it, so nothing
* a handler reaches synchronously can re-base a message's deadlines.
* Both deadlines are absolute from that start: received bytes never
* extend them, so a client trickling data slowly cannot hold the
* socket past them. */
us_socket_timeout(s, nodeRemainingReceiveSeconds(s, httpResponseData, phase));
Comment thread
claude[bot] marked this conversation as resolved.
return true;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Comment thread
claude[bot] marked this conversation as resolved.
private:

/* Not constexpr — the ordinals are linked from `src/uws_sys/SocketKind.rs`
* so a reorder there can't silently mis-route us. Only ever read
* at runtime (listen/adopt). */
Expand Down Expand Up @@ -172,6 +254,13 @@ struct HttpContext {
* surfaced separately (rejectUnauthorized above / tls.authorized). */
httpResponseData->isAuthorized = success;

if (httpResponseData->hasNodeReceiveTimeouts) {
/* Like Node ('secureConnection' → parser.initialize →
* last_message_start_), the first message's receive deadlines
* start after the TLS handshake, not at TCP accept. */
httpResponseData->nodeMessageStartMs = nodeNowMs();
}

/* Any connected socket should timeout until it has a request */
((HttpResponse<SSL> *) s)->resetTimeout();

Expand All @@ -184,13 +273,22 @@ struct HttpContext {

static us_socket_t *onOpen(us_socket_t *s, int /*is_client*/, char * /*ip*/, int /*ip_length*/) {
/* Init socket ext */
new (us_socket_ext(s)) HttpResponseData<SSL>;
/* Any connected socket should timeout until it has a request */
auto *httpResponseData = new (us_socket_ext(s)) HttpResponseData<SSL>;
HttpContextData<SSL> *httpContextData = getSocketContextDataS(s);
/* Any connected socket should timeout until it has a request */
if (httpContextData->flags.hasNodeReceiveTimeouts) {
/* node:http: no implicit idle timeout; the headersTimeout receive
* deadline owns the socket timer until a complete request arrives. */
httpResponseData->hasNodeReceiveTimeouts = true;
httpResponseData->idleTimeout = 0;
/* The first message's receive deadlines are measured from
* connection open until its first byte arrives and re-bases them. */
httpResponseData->nodeMessageStartMs = nodeNowMs();
}
Comment thread
claude[bot] marked this conversation as resolved.
((HttpResponse<SSL> *) s)->resetTimeout();
Comment thread
claude[bot] marked this conversation as resolved.

if(!SSL) {
/* Call filter */
HttpContextData<SSL> *httpContextData = getSocketContextDataS(s);
for (auto &f : httpContextData->filterHandlers) {
f((HttpResponse<SSL> *) s, 1);
}
Expand Down Expand Up @@ -268,6 +366,13 @@ struct HttpContext {
httpContextData->flags.isParsingHttp = true;
httpResponseData->isIdle = false;

if (httpResponseData->hasNodeReceiveTimeouts) {
/* Every message whose request line begins in this packet starts its
* receive deadlines here; the parser stamps nodeMessageStartMs with
* this as it reaches each one (see HttpParser). */
httpResponseData->nodePacketTimestampMs = nodeNowMs();
}

// clients need to know the cursor after http parse, not servers!
// how far did we read then? we need to know to continue with websocket parsing data? or?

Expand Down Expand Up @@ -365,7 +470,6 @@ struct HttpContext {

}, [httpResponseData, httpContextData](void *user, std::string_view data, bool fin) -> void * {


if (httpResponseData->isConnectRequest && httpResponseData->socketData && httpContextData->onSocketData) {
httpContextData->onSocketData(httpResponseData->socketData, SSL, (struct us_socket_t *) user, data.data(), data.length(), fin);
}
Expand Down Expand Up @@ -437,6 +541,12 @@ struct HttpContext {
((HttpResponse<SSL> *) s)->resetTimeout();
}

if (httpContextData->flags.hasNodeReceiveTimeouts) {
/* Receive-phase deadline, or restore the idle timeout (which
* disarms a body deadline once the message is complete). */
((HttpResponse<SSL> *) s)->resetTimeout();
}

/* We need to check if we should close this socket here now */
if (httpResponseData->state & HttpResponseData<SSL>::HTTP_CONNECTION_CLOSE) {
if ((httpResponseData->state & HttpResponseData<SSL>::HTTP_RESPONSE_PENDING) == 0) {
Expand Down Expand Up @@ -514,8 +624,13 @@ struct HttpContext {
}
/* Ask the developer to write data and return success (true) or failure (false), OR skip sending anything and return success (true). */
if (httpResponseData->onWritable) {
/* We are now writable, so hang timeout again, the user does not have to do anything so we should hang until end or tryEnd rearms timeout */
us_socket_timeout(s, 0);
/* We are now writable, so hang timeout again, the user does not have to do anything so we should hang until end or tryEnd rearms timeout.
* node:http receive deadlines own the timer while a message is being
* received and tryArmNodeReceiveTimeout assumes it stays armed, so
* never suspend it here. */
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!httpResponseData->hasNodeReceiveTimeouts || nodeReceivePhase(s, httpResponseData) == NodeReceivePhase::None) {
us_socket_timeout(s, 0);
}

/* We expect the developer to return whether or not write was successful (true).
* If write was never called, the developer should still return true so that we may drain. */
Expand Down Expand Up @@ -565,6 +680,37 @@ struct HttpContext {
AsyncSocket<SSL> *asyncSocket = reinterpret_cast<AsyncSocket<SSL> *>(s);
// Node.js by default closes the connection but they emit the timeout event before that
HttpResponseData<SSL> *httpResponseData = reinterpret_cast<HttpResponseData<SSL> *>(asyncSocket->getAsyncSocketData());
HttpContextData<SSL> *httpContextData = getSocketContextDataS(s);

/* node:http headersTimeout/requestTimeout expiry: like Node, always
* emit 'clientError' (ERR_HTTP_REQUEST_TIMEOUT) and close; the canned
* 408 is only written when nothing was written for the current message
* (Node: socketOnError's bytesWritten check). No 'timeout' events are
* emitted for these deadlines. The write-state bits are only meaningful
* for the dispatched (Body phase) message; in the Headers phase they
* are leftovers from the previous keep-alive response.
* For SSL, Node's HTTP layer only attaches after the handshake: a stalled
* handshake (isAuthorized still false, set only by onHandshake) must not be
* reported as an HTTP request timeout — it falls through to a plain close,
* still bounded by the deadline onOpen armed. */
auto nodePhase = httpResponseData->hasNodeReceiveTimeouts && (!SSL || httpResponseData->isAuthorized)
? nodeReceivePhase(s, httpResponseData) : NodeReceivePhase::None;
if (nodePhase != NodeReceivePhase::None) {
if (httpContextData->onClientError) {
httpContextData->onClientError(SSL, s, HTTP_PARSER_ERROR_REQUEST_TIMEOUT, nullptr, 0);
}
/* The 'clientError' listener may have closed the socket already */
if (us_socket_is_closed(s)) {
return s;
}
bool wroteSomething = nodePhase == NodeReceivePhase::Body
&& (httpResponseData->state & (HttpResponseData<SSL>::HTTP_STATUS_CALLED | HttpResponseData<SSL>::HTTP_WRITE_CALLED | HttpResponseData<SSL>::HTTP_END_CALLED));
if (!wroteSomething && !us_socket_is_shut_down(s)) {
us_socket_write(s, httpErrorResponses[HTTP_ERROR_408_REQUEST_TIMEOUT].data(), (int) httpErrorResponses[HTTP_ERROR_408_REQUEST_TIMEOUT].length());
us_socket_shutdown(s);
}
Comment thread
claude[bot] marked this conversation as resolved.
return asyncSocket->close();
}

if (httpResponseData->onTimeout) {
httpResponseData->onTimeout((HttpResponse<SSL> *)s, httpResponseData->userData);
Expand Down
9 changes: 9 additions & 0 deletions packages/bun-uws/src/HttpContextData.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ struct HttpFlags {
bool requireHostHeader: 1 = true;
bool isAuthorized: 1 = false;
bool useStrictMethodValidation: 1 = false;
/* node:http only: per-socket receive deadlines (headersTimeout /
* requestTimeout) drive the socket timer instead of the legacy 10s
* pre-request idle timeout. Never set for Bun.serve. */
bool hasNodeReceiveTimeouts: 1 = false;
};

template <bool SSL>
Expand Down Expand Up @@ -71,6 +75,11 @@ struct alignas(16) HttpContextData {

uint64_t maxHeaderSize = 0; // 0 means no limit

/* node:http receive deadlines in seconds (0 = disabled); only read when
* flags.hasNodeReceiveTimeouts is set. */
unsigned int nodeHeadersTimeoutSeconds = 0;
unsigned int nodeRequestTimeoutSeconds = 0;

// TODO: SNI
void clearRoutes() {
this->router = HttpRouter<RouterData>{};
Expand Down
6 changes: 4 additions & 2 deletions packages/bun-uws/src/HttpErrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ namespace uWS {
enum HttpError {
HTTP_ERROR_505_HTTP_VERSION_NOT_SUPPORTED = 1,
HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 2,
HTTP_ERROR_400_BAD_REQUEST = 3
HTTP_ERROR_400_BAD_REQUEST = 3,
HTTP_ERROR_408_REQUEST_TIMEOUT = 4
};


Expand All @@ -33,7 +34,8 @@ static const std::string_view httpErrorResponses[] = {
"", /* Zeroth place is no error so don't use it */
"HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\n\r\n",
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n",
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n",
"HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n"
};


Expand Down
40 changes: 40 additions & 0 deletions packages/bun-uws/src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,19 @@ namespace uWS
HTTP_PARSER_ERROR_INVALID_EOF = 8,
HTTP_PARSER_ERROR_INVALID_METHOD = 9,
HTTP_PARSER_ERROR_INVALID_HEADER_TOKEN = 10,
/* Not a parse error: a node:http headersTimeout/requestTimeout receive
* deadline expired before the message was fully received. */
HTTP_PARSER_ERROR_REQUEST_TIMEOUT = 11,
};

/* node:http receive phase of a socket (only meaningful when
* hasNodeReceiveTimeouts is set): Headers from connection (or the first
* byte of the next keep-alive message) until a message's header section
* is fully parsed, Body until its body is fully received, None while a
* handler/response is in flight or the keep-alive socket is idle after a
* completed exchange. CONNECT tunnels stream forever and have no phase. */
enum class NodeReceivePhase : unsigned char { None, Headers, Body };
Comment on lines +73 to +79

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Shorten the phase comment to the repository limit.

This new block is 6 lines; keep only the durable invariant.

Proposed cleanup
-    /* node:http receive phase of a socket (only meaningful when
-     * hasNodeReceiveTimeouts is set): Headers from connection (or the first
-     * byte of the next keep-alive message) until a message's header section
-     * is fully parsed, Body until its body is fully received, None while a
-     * handler/response is in flight or the keep-alive socket is idle after a
-     * completed exchange. CONNECT tunnels stream forever and have no phase. */
+    /* node:http receive phase when hasNodeReceiveTimeouts is set.
+     * Headers lasts until parsed; Body until received; CONNECT/idle use None. */

As per coding guidelines, “Keep code comments to 3 lines max.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* node:http receive phase of a socket (only meaningful when
* hasNodeReceiveTimeouts is set): Headers from connection (or the first
* byte of the next keep-alive message) until a message's header section
* is fully parsed, Body until its body is fully received, None while a
* handler/response is in flight or the keep-alive socket is idle after a
* completed exchange. CONNECT tunnels stream forever and have no phase. */
enum class NodeReceivePhase : unsigned char { None, Headers, Body };
/* node:http receive phase when hasNodeReceiveTimeouts is set.
* Headers lasts until parsed; Body until received; CONNECT/idle use None. */
enum class NodeReceivePhase : unsigned char { None, Headers, Body };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bun-uws/src/HttpParser.h` around lines 73 - 79, The NodeReceivePhase
comment in HttpParser.h is too long and exceeds the repository’s 3-line comment
limit. Trim the block to the durable invariant only, keeping the explanation
around NodeReceivePhase and hasNodeReceiveTimeouts while removing the extra
examples and situational detail; update the comment near the NodeReceivePhase
enum so it stays concise and under 3 lines.

Source: Coding guidelines



enum HTTPHeaderParserError: uint8_t {
HTTP_HEADER_PARSER_ERROR_NONE = 0,
Expand Down Expand Up @@ -868,6 +879,15 @@ namespace uWS
data[length + 1] = 'a'; /* Anything that is not \n, to trigger "invalid request" */
req->ancientHttp = false;
for (;length;) {
/* node:http: a message whose request line begins here arrived in the
* packet onData is delivering, so its receive deadlines are measured
* from that packet's arrival. ConsumeMinimally only ever resumes a
* message buffered from an earlier packet, which keeps its own start. */
if constexpr (!ConsumeMinimally) {
if (nodePacketTimestampMs) {
nodeMessageStartMs = nodePacketTimestampMs;
}
}
auto result = getHeaders(data, data + length, req->headers, reserved, req->ancientHttp, isConnectRequest, useStrictMethodValidation, maxHeaderSize);
if(result.isError()) {
return result;
Expand Down Expand Up @@ -1039,6 +1059,26 @@ namespace uWS
}

public:
/* True while a dispatched message still has body bytes outstanding
* (Content-Length not yet satisfied, or chunked body not terminated). */
bool isReceivingHttpBody() const {
return remainingStreamingBytes != 0;
}

/* True when incomplete request bytes are buffered, i.e. a message started
* arriving but its header section has not been fully parsed yet. */
bool hasPartialRequest() const {
return fallback.length() > 0;
}

/* node:http receive deadlines are absolute per message, measured from the
* message's first received byte. HttpContext::onData stamps the packet's
* arrival time here before parsing (0 = those deadlines are off), and the
* parser copies it into nodeMessageStartMs as each message's request line
* begins. See HttpContext::tryArmNodeReceiveTimeout. */
uint64_t nodePacketTimestampMs = 0;
uint64_t nodeMessageStartMs = 0;

HttpParserResult consumePostPadded(uint64_t maxHeaderSize, bool& isConnectRequest, bool requireHostHeader, bool useStrictMethodValidation, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
/* This resets BloomFilter by construction, but later we also reset it again.
* Optimize this to skip resetting twice (req could be made global) */
Expand Down
Loading
Loading