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
190 changes: 170 additions & 20 deletions packages/bun-uws/src/HttpResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@

#include "MoveOnlyFunction.h"

#include <array>
#include <cctype>
#include <string_view>

/* todo: tryWrite is missing currently, only send smaller segments with write */

namespace uWS {
Expand All @@ -42,6 +46,61 @@ template <bool, bool, typename> struct WebSocketContext;
/* Some pre-defined status constants to use with writeStatus */
inline constexpr const char* HTTP_200_OK = "200 OK";

/* `Connection: keep-alive\r\nKeep-Alive: timeout=N\r\n`, one rendered line
* pair per idle timeout, indexed by the timeout in sweep ticks
* ((seconds + 3) >> 2, the rounding us_socket_timeout applies).
*
* N is the idle time the socket is sure to survive, not the configured
* seconds: the sweep timer runs every LIBUS_TIMEOUT_GRANULARITY seconds
* and a timeout of T ticks fires between (T - 1) and T sweeps after it was
* armed. A client that reads N retires the socket before the server can
* close it (Node's http.Agent and undici both subtract a margin from N).
* The default 10 s timeout advertises 8. Entry 0 (no idle timeout) and
* entry 1 (the socket can close at the next sweep, so no N is safe) are
* the Connection line alone. */
struct KeepAliveHeaderLines {
static constexpr unsigned CONNECTION_LINE_LENGTH = 24; /* "Connection: keep-alive\r\n" */
char data[50];
unsigned char length;
};

/* Connection is `1#connection-option` (RFC 9112 9.3): is `close` one of the
* comma-separated tokens, in any case? */
inline bool connectionValueHasClose(std::string_view value) {
for (size_t i = 0; i + 5 <= value.length(); i++) {
if (strncasecmp(value.data() + i, "close", 5)) {
continue;
}
bool leftOk = i == 0 || !isalnum((unsigned char) value[i - 1]);
bool rightOk = i + 5 == value.length() || !isalnum((unsigned char) value[i + 5]);
if (leftOk && rightOk) {
return true;
}
}
return false;
}
Comment thread
robobun marked this conversation as resolved.

inline constexpr auto keepAliveHeaderLines = [] {
std::array<KeepAliveHeaderLines, 65> lines{};
for (unsigned ticks = 0; ticks < lines.size(); ticks++) {
KeepAliveHeaderLines &line = lines[ticks];
unsigned n = 0;
for (char c : std::string_view("Connection: keep-alive\r\n")) line.data[n++] = c;
if (ticks > 1) {
for (char c : std::string_view("Keep-Alive: timeout=")) line.data[n++] = c;
unsigned seconds = (ticks - 1) * LIBUS_TIMEOUT_GRANULARITY;
char digits[3];
unsigned d = 0;
do { digits[d++] = (char) ('0' + seconds % 10); seconds /= 10; } while (seconds);
while (d) line.data[n++] = digits[--d];
line.data[n++] = '\r';
line.data[n++] = '\n';
}
line.length = (unsigned char) n;
}
return lines;
}();

template <bool SSL>
struct HttpResponse : public AsyncSocket<SSL> {
/* Solely used for getHttpResponseData() */
Expand Down Expand Up @@ -82,14 +141,35 @@ struct HttpResponse : public AsyncSocket<SSL> {
Super::write(buf, length);
}

/* Called only once per request */
/* The headers the server adds on its own. Every terminator calls this
* right before the CRLF that ends the header section, after the caller's
* headers, so each decision below sees them. Idempotent. */
void writeMark() {
if (getHttpResponseData()->state & HttpResponseData<SSL>::HTTP_WROTE_DATE_HEADER) {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();
if (!(httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_DATE_HEADER)) {
/* Date is always written */
writeHeader("Date", std::string_view(((LoopData *) us_loop_ext(us_socket_group_loop(us_socket_group((us_socket_t *) this))))->date, 29));
httpResponseData->state |= HttpResponseData<SSL>::HTTP_WROTE_DATE_HEADER;
}

/* Connection: keep-alive and Keep-Alive: timeout=N on a response that
* leaves the connection open, as node:http sends them. Skipped when the
* caller wrote either header (see the bits) or the connection closes
* after this response (HTTP/1.0, Connection: close, peer FIN,
* close-when-idle). */
constexpr uint32_t wroteBoth = HttpResponseData<SSL>::HTTP_WROTE_CONNECTION_HEADER | HttpResponseData<SSL>::HTTP_WROTE_KEEP_ALIVE_HEADER;
uint32_t wrote = httpResponseData->state & wroteBoth;
if (wrote == wroteBoth) {
return;
}
httpResponseData->state |= wroteBoth;
if (httpResponseData->closesAfterResponse()) {
return;
}
/* Date is always written */
writeHeader("Date", std::string_view(((LoopData *) us_loop_ext(us_socket_group_loop(us_socket_group((us_socket_t *) this))))->date, 29));
getHttpResponseData()->state |= HttpResponseData<SSL>::HTTP_WROTE_DATE_HEADER;
const KeepAliveHeaderLines &lines = keepAliveHeaderLines[(httpResponseData->idleTimeout + 3) >> 2];
/* A caller's Keep-Alive header keeps its value; only the Connection line is missing. */
int length = wrote ? (int) KeepAliveHeaderLines::CONNECTION_LINE_LENGTH : (int) lines.length;
Super::write(lines.data, length);
Comment thread
robobun marked this conversation as resolved.
}

/* Shutdown+close when the connection is marked to close (Connection:
Expand Down Expand Up @@ -158,6 +238,20 @@ struct HttpResponse : public AsyncSocket<SSL> {
httpResponseData->markDone(this);
}

/* The connection closes once this response is out (RFC 9112 9.6). HTTP/1.1
* must say so unless the client already did, so the header is written
* while the header section is open and no Connection header is on the
* wire yet. The bit is what the close gates read; it has to be set before
* writeMark() seals the headers, or the response advertises keep-alive
* on a connection about to close. */
void markConnectionClose() {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();
if (!(httpResponseData->state & (HttpResponseData<SSL>::HTTP_CONNECTION_CLOSE | HttpResponseData<SSL>::HTTP_WRITE_CALLED | HttpResponseData<SSL>::HTTP_END_CALLED))) {
writeHeader("Connection", "close");
}
httpResponseData->state |= HttpResponseData<SSL>::HTTP_CONNECTION_CLOSE;
}

/* Returns true on success, indicating that it might be feasible to write more data.
* Will start timeout if stream reaches totalSize or write failure. */
bool internalEnd(std::string_view data, uint64_t totalSize, bool optional, bool allowContentLength = true, bool closeConnection = false) {
Expand All @@ -181,21 +275,8 @@ struct HttpResponse : public AsyncSocket<SSL> {
}

/* In some cases, such as when refusing huge data we want to close the connection when drained */
if (closeConnection) {
/* We can only write the header once */
if (!(httpResponseData->state & (HttpResponseData<SSL>::HTTP_END_CALLED))) {

/* HTTP 1.1 must send this back unless the client already sent it to us.
* It is a connection close when either of the two parties say so but the
* one party must tell the other one so.
*
* This check also serves to limit writing the header only once. */
if ((httpResponseData->state & HttpResponseData<SSL>::HTTP_CONNECTION_CLOSE) == 0 && !(httpResponseData->state & (HttpResponseData<SSL>::HTTP_WRITE_CALLED))) {
writeHeader("Connection", "close");
}

httpResponseData->state |= HttpResponseData<SSL>::HTTP_CONNECTION_CLOSE;
}
if (closeConnection && !(httpResponseData->state & HttpResponseData<SSL>::HTTP_END_CALLED)) {
markConnectionClose();
}

/* if write was called and there was previously no Content-Length header set.
Expand Down Expand Up @@ -546,6 +627,28 @@ struct HttpResponse : public AsyncSocket<SSL> {
HttpResponse *writeHeader(std::string_view key, std::string_view value) {
writeStatus(HTTP_200_OK);

/* Every Connection or Keep-Alive header a caller writes (a user
* header from any route, the 101 Upgrade) passes here, so this is
* where writeMark() learns not to add its own, and where a `close`
* token marks the connection to close once the response is out
* (RFC 9112 9.6). A caller that claimed the Connection header before
* writing any (node:http, which closes from JavaScript after 'finish')
* keeps its own semantics. Both names are 10 bytes; the length test
* is all that other headers pay. */
if (key.length() == 10) [[unlikely]] {
if (!strncasecmp(key.data(), "connection", 10)) {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();
if (!(httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_CONNECTION_HEADER)) {
httpResponseData->state |= HttpResponseData<SSL>::HTTP_WROTE_CONNECTION_HEADER | HttpResponseData<SSL>::HTTP_WROTE_KEEP_ALIVE_HEADER;
if (connectionValueHasClose(value)) {
httpResponseData->state |= HttpResponseData<SSL>::HTTP_CONNECTION_CLOSE;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
} else if (!strncasecmp(key.data(), "keep-alive", 10)) {
getHttpResponseData()->state |= HttpResponseData<SSL>::HTTP_WROTE_KEEP_ALIVE_HEADER;
}
}

Super::write(key.data(), (int) key.length());
Super::write(": ", 2);
Super::write(value.data(), (int) value.length());
Expand Down Expand Up @@ -601,6 +704,9 @@ struct HttpResponse : public AsyncSocket<SSL> {
!(httpResponseData->state & (HttpResponseData<SSL>::HTTP_WRITE_CALLED | HttpResponseData<SSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER
| HttpResponseData<SSL>::HTTP_ANCIENT_REQUEST | HttpResponseData<SSL>::HTTP_NO_BODY_STATUS))) {
writeStatus(HTTP_200_OK);
if (closeConnection) {
markConnectionClose();
}
writeMark();
Super::write("\r\n", 2);
httpResponseData->state |= HttpResponseData<SSL>::HTTP_WRITE_CALLED;
Expand All @@ -612,6 +718,47 @@ struct HttpResponse : public AsyncSocket<SSL> {
internalEnd(data, data.length(), false, !(httpResponseData->state & (HttpResponseData<SSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER | HttpResponseData<SSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER)), closeConnection);
}

/* End the response with no body bytes and no body framing: a HEAD
* response, a 304, a 307/308 file route. The caller has written the
* status and its headers, Content-Length included, itself.
*
* Once write()/flushHeaders() (HTTP_WRITE_CALLED) or an earlier end
* (HTTP_END_CALLED) terminated the header section, header bytes written
* here would land inside the body (node:http res.destroy() mid-response
* ends up here). Setting HTTP_CONNECTION_CLOSE is what makes the close
* gates tear the connection down; the header itself is only advisory,
* same as in internalEnd().
*
* No close gate here: callers (FileResponseStream::finish,
* DevServer/HTMLBundle error paths) keep using the response after this
* returns, so closing inside this call would destruct the ext under
* them. Corked callers get the cork() wrapper's post-uncork gate;
* uncorked ones run closeIfDoneAndMarked() themselves once they are
* done with the response. */
void endWithoutBody(bool closeConnection) {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();
bool headersOpen = !(httpResponseData->state & (HttpResponseData<SSL>::HTTP_WRITE_CALLED | HttpResponseData<SSL>::HTTP_END_CALLED));
/* Decided before the close header below, which writes a status of its
* own: a caller that ends a response it never started (node:http
* res.destroy() before writeHead, an HTML route that lost its server)
* gets the header section it always got, without the server headers. */
bool hasStatus = httpResponseData->state & HttpResponseData<SSL>::HTTP_STATUS_CALLED;
if (closeConnection) {
markConnectionClose();
}
if (headersOpen) {
if (hasStatus) {
writeMark();
}
/* Some HTTP clients require the complete "<header>\r\n\r\n" to be
* sent. If not, they may throw a ConnectionError. */
Super::write("\r\n", 2);
}
httpResponseData->state |= HttpResponseData<SSL>::HTTP_END_CALLED;
httpResponseData->markDone(this);
this->resetTimeout();
}

/* Try and end the response. Returns [true, true] on success.
* Starts a timeout in some cases. Returns [ok, hasResponded] */
std::pair<bool, bool> tryEnd(std::string_view data, uintmax_t totalSize = 0, bool closeConnection = false) {
Expand All @@ -629,6 +776,9 @@ struct HttpResponse : public AsyncSocket<SSL> {
bool sendTerminatingChunk(bool closeConnection = false) {
writeStatus(HTTP_200_OK);
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();
if (closeConnection) {
markConnectionClose();
}
if (!(httpResponseData->state & (HttpResponseData<SSL>::HTTP_WRITE_CALLED | HttpResponseData<SSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER))) {
/* Write mark on first call to write */
writeMark();
Expand Down
19 changes: 19 additions & 0 deletions packages/bun-uws/src/HttpResponseData.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
/* node:http: the peer sent its FIN first (HTTP_NODE_RECEIVED_FIN only covers a
* deferred close). onSocketClosed reports it so the JS socket emits 'end'. */
HTTP_NODE_PEER_ENDED = 1 << 19,
/* A Connection header is on the wire for this response: the caller
* wrote one (a user header, node:http's own line, the 101 Upgrade), or
* writeMark() decided. Stops writeMark() from adding
* `Connection: keep-alive`. A caller's Connection header also sets
* HTTP_WROTE_KEEP_ALIVE_HEADER: like Node, the server then sends no
* Keep-Alive hint of its own. */
HTTP_WROTE_CONNECTION_HEADER = 1 << 20,
/* Same for the Keep-Alive header. A caller's Keep-Alive header sets
* only this bit, so writeMark() still adds `Connection: keep-alive`. */
HTTP_WROTE_KEEP_ALIVE_HEADER = 1 << 21,

/* Bits that describe the connection rather than the response in flight.
* There is one HttpResponseData per socket, reused by every request on a
Expand Down Expand Up @@ -241,6 +251,15 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
|| ((state & HTTP_NODE_RECEIVED_FIN) && nodeHttpQueuedPipelinedCount == 0)
|| ((state & HTTP_CLOSE_WHEN_IDLE) && this->isIdle);
}

/* Whether the connection closes once the response in flight is out, so
* the response must not advertise keep-alive. Unlike
* shouldCloseConnection() this is asked while the response is still
* being written (isIdle is false then), so a close-when-idle mark counts
* on its own. */
bool closesAfterResponse() const {
return state & (HTTP_CONNECTION_CLOSE | HTTP_NODE_RECEIVED_FIN | HTTP_CLOSE_WHEN_IDLE);
}
};

/* Per-connection state that only node:http compat servers need.
Expand Down
43 changes: 7 additions & 36 deletions src/jsc/bindings/NodeHTTP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -379,23 +379,6 @@ static void writeResponseHeader(uWS::HttpResponse<isSSL>* res, const WTF::String
res->writeHeader(nameBytes.view(), valueBytes.view());
}

// Connection is `1#connection-option` (RFC 9112 §9.3): look for the "close"
// token anywhere in a comma-separated list. Mirrors the /(?:^|\W)close(?:$|\W)/i
// check used by Bun's node:http layer.
static bool connectionValueHasClose(const WTF::String& value)
{
size_t pos = 0;
while ((pos = value.findIgnoringASCIICase("close"_s, pos)) != WTF::notFound) {
bool leftOk = pos == 0 || !isASCIIAlphanumeric(value[pos - 1]);
size_t end = pos + 5;
bool rightOk = end >= value.length() || !isASCIIAlphanumeric(value[end]);
if (leftOk && rightOk)
return true;
pos = end;
}
return false;
}

template<bool isSSL>
static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::HttpResponse<isSSL>* res)
{
Expand Down Expand Up @@ -432,10 +415,7 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::
// <
//
if (header.key == WebCore::HTTPHeaderName::ContentLength) {
if (!(data->state & uWS::HttpResponseData<isSSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER)) {
data->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER;
res->writeMark();
}
data->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER;
}

// Prevent automatic Date header insertion when user provides one
Expand All @@ -448,13 +428,6 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::
data->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER;
}

// RFC 9112 §9.6: a server that sends the "close" connection option MUST
// close the connection after the response. Mark the uWS state so
// end()/tryEnd() shut the socket down instead of returning it to the
// keep-alive pool.
if (header.key == WebCore::HTTPHeaderName::Connection && connectionValueHasClose(value)) {
data->state |= uWS::HttpResponseData<isSSL>::HTTP_CONNECTION_CLOSE;
}
writeResponseHeader<isSSL>(res, name, value);
Comment thread
robobun marked this conversation as resolved.
}

Expand Down Expand Up @@ -563,10 +536,11 @@ static void NodeHTTPServer__writeHead(
}
response->writeStatus(std::string_view(statusMessage, statusMessageLength));

// node:http's ServerResponse owns the Date header entirely (it honors
// res.sendDate / removeHeader("date") in JS), so never let uWS write its
// own Date header for these responses.
response->getHttpResponseData()->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_DATE_HEADER;
// node:http's ServerResponse renders Date, Connection and Keep-Alive
// itself (autoHeaderBits), so uWS must not add its own.
Comment thread
robobun marked this conversation as resolved.
response->getHttpResponseData()->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_DATE_HEADER
| uWS::HttpResponseData<isSSL>::HTTP_WROTE_CONNECTION_HEADER
| uWS::HttpResponseData<isSSL>::HTTP_WROTE_KEEP_ALIVE_HEADER;

// 204/304 responses must not carry any body framing, even when the user
// explicitly set a Transfer-Encoding header (Node.js suppresses the
Expand Down Expand Up @@ -618,10 +592,7 @@ static void NodeHTTPServer__writeHead(
WebCore::HTTPHeaderName headerName;
if (WebCore::findHTTPHeaderName(StringView(name), headerName)) {
if (headerName == WebCore::HTTPHeaderName::ContentLength) {
if (!(httpResponseData->state & uWS::HttpResponseData<isSSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER)) {
httpResponseData->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER;
response->writeMark();
}
httpResponseData->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER;
} else if (headerName == WebCore::HTTPHeaderName::Date) {
httpResponseData->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_DATE_HEADER;
} else if (headerName == WebCore::HTTPHeaderName::TransferEncoding) {
Expand Down
Loading
Loading