Skip to content
Closed
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
30 changes: 24 additions & 6 deletions packages/bun-uws/src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,23 @@ struct HttpContext {
}

private:
/* Best-effort write of a parser-tier error response (400/413/414/431/501/505)
* and connection teardown. RFC 9110 6.6.1 says an origin server MUST send
* Date on every response if it has a clock, so these carry the loop's cached
* Date and an explicit Content-Length: 0 like every handler-path response. */
static void writeHttpErrorAndClose(us_socket_t *s, unsigned int httpErrorStatusCode) {
std::string_view status = httpErrorResponses[httpErrorStatusCode];
const char *date = ((LoopData *) us_loop_ext(us_socket_group_loop(us_socket_group(s))))->date;
/* "HTTP/1.1 nnn Reason" (<= 44) + fixed headers (65) + date (29) + CRLFCRLF */
char buf[160];
int n = snprintf(buf, sizeof(buf),
"%.*s\r\nConnection: close\r\nContent-Length: 0\r\nDate: %.29s\r\n\r\n",
(int) status.length(), status.data(), date);
us_socket_write(s, buf, n);
us_socket_shutdown(s);
us_socket_close(s, 0, nullptr);
}

/* ── vtable handlers ─────────────────────────────────────────────────── */

static void onHandshake(us_socket_t *s, int success, struct us_bun_verify_error_t verify_error, void * /*custom_data*/) {
Expand Down Expand Up @@ -436,8 +453,12 @@ struct HttpContext {
/* Route the method and URL */
selectedRouter->getUserData() = {(HttpResponse<SSL> *) s, httpRequest};
if (!selectedRouter->route(httpRequest->getCaseSensitiveMethod(), httpRequest->getUrlForRouting())) {
/* We have to force close this socket as we have no handler for it */
us_socket_close((us_socket_t *) s, 0, nullptr);
/* No handler for this method: Bun.serve always registers a wildcard
* handler for every method in supportedHttpMethods (and node:http
* registers ANY directly), so a miss here means a method token
* outside that table. RFC 9110 9.1: an unrecognized method SHOULD
* get 501. */
writeHttpErrorAndClose((us_socket_t *) s, HTTP_ERROR_501_NOT_IMPLEMENTED);
return nullptr;
}

Expand Down Expand Up @@ -577,10 +598,7 @@ struct HttpContext {
httpContextData->onClientError(SSL, s, result.parserError, data, length);
}
/* For errors, we only deliver them "at most once". We don't care if they get halfways delivered or not. */
us_socket_write(s, httpErrorResponses[httpErrorStatusCode].data(), (int) httpErrorResponses[httpErrorStatusCode].length());
us_socket_shutdown(s);
/* Close any socket on HTTP errors */
us_socket_close(s, 0, nullptr);
writeHttpErrorAndClose(s, httpErrorStatusCode);
}

auto returnedData = result.returnedData;
Expand Down
17 changes: 11 additions & 6 deletions packages/bun-uws/src/HttpErrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,22 @@ 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_413_PAYLOAD_TOO_LARGE = 4
HTTP_ERROR_413_PAYLOAD_TOO_LARGE = 4,
HTTP_ERROR_414_URI_TOO_LONG = 5,
HTTP_ERROR_501_NOT_IMPLEMENTED = 6
};


/* Anonymized pages */
/* Status lines (no CRLF) indexed by HttpError; the sender adds Date,
* Content-Length: 0 and Connection: close. */
inline constexpr 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 413 Payload Too Large\r\nConnection: close\r\n\r\n"
"HTTP/1.1 505 HTTP Version Not Supported",
"HTTP/1.1 431 Request Header Fields Too Large",
"HTTP/1.1 400 Bad Request",
"HTTP/1.1 413 Payload Too Large",
"HTTP/1.1 414 URI Too Long",
"HTTP/1.1 501 Not Implemented"
};


Expand Down
27 changes: 23 additions & 4 deletions packages/bun-uws/src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ struct HttpResponseData;
HTTP_HEADER_PARSER_ERROR_INVALID_METHOD = 3,
HTTP_HEADER_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE = 4,
HTTP_HEADER_PARSER_ERROR_PAUSED_H2_UPGRADE = 5,
HTTP_HEADER_PARSER_ERROR_REQUEST_URI_TOO_LONG = 6,
};

struct HttpParserResult {
Expand Down Expand Up @@ -596,6 +597,15 @@ struct HttpResponseData;

const size_t MAX_FALLBACK_SIZE = BUN_DEFAULT_MAX_HTTP_HEADER_SIZE;

/* Distinguish 414 from 431 when an unparsed request head exceeds the
* limit: no LF in the buffered bytes means the request-line itself has
* not ended. */
static unsigned int headOverflowStatus(const char *data, size_t length) {
return memchr(data, '\n', length) == nullptr
? HTTP_ERROR_414_URI_TOO_LONG
: HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE;
}

/* Maximum chunk-extension bytes per chunk, matching Node/llhttp's
* kMaxChunkExtensionsSize (16 KiB). Enforced for every server
* personality so a client cannot stream unbounded extension bytes. */
Expand Down Expand Up @@ -826,12 +836,12 @@ struct HttpResponseData;
uint64_t word;
memcpy(&word, data, sizeof(uint64_t));
if(maxHeaderSize && (uintptr_t)(data - start) > maxHeaderSize) {
return ConsumeRequestLineResult::error(HTTP_HEADER_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
return ConsumeRequestLineResult::error(HTTP_HEADER_PARSER_ERROR_REQUEST_URI_TOO_LONG);
}
if (hasLess(word, 33)) {
while (*(unsigned char *)data > 32) data++;
if(maxHeaderSize && (uintptr_t)(data - start) > maxHeaderSize) {
return ConsumeRequestLineResult::error(HTTP_HEADER_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
return ConsumeRequestLineResult::error(HTTP_HEADER_PARSER_ERROR_REQUEST_URI_TOO_LONG);
}
/* Now we stand on space */
header.value = {start, (size_t) (data - start)};
Expand Down Expand Up @@ -951,6 +961,11 @@ struct HttpResponseData;
return HttpParserResult::error(HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_METHOD);
case HTTP_HEADER_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE:
return HttpParserResult::error(HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
case HTTP_HEADER_PARSER_ERROR_REQUEST_URI_TOO_LONG:
/* 414 for the response status; the clientError code stays
* HEADER_FIELDS_TOO_LARGE (llhttp reports HPE_HEADER_OVERFLOW
* for an oversize request line too). */
return HttpParserResult::error(HTTP_ERROR_414_URI_TOO_LONG, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
case HTTP_HEADER_PARSER_ERROR_PAUSED_H2_UPGRADE:
return HttpParserResult::error(HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_PAUSED_H2_UPGRADE);
default: {
Expand Down Expand Up @@ -1149,6 +1164,10 @@ struct HttpResponseData;
/* Even if we could parse it, check for length here as well */
const uint64_t maxBufferedHeaderSize = maxHeaderSize ? maxHeaderSize : MAX_FALLBACK_SIZE;
if (consumed > maxBufferedHeaderSize) {
/* RFC 9110 15.5.15: 414 when the request-target itself is the overflow. */
if (req->headers[0].value.length() > maxBufferedHeaderSize) {
return HttpParserResult::error(HTTP_ERROR_414_URI_TOO_LONG, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
}
return HttpParserResult::error(HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
}

Expand Down Expand Up @@ -1518,7 +1537,7 @@ struct HttpResponseData;

} else {
if (fallback.length() == maxFallbackSize) {
return HttpParserResult::error(HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
return HttpParserResult::error(headOverflowStatus(fallback.data(), fallback.length()), HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
}
return HttpParserResult::success(0, user);
}
Expand All @@ -1539,7 +1558,7 @@ struct HttpResponseData;
if (length < maxFallbackSize) {
fallback.append(data, length);
} else {
return HttpParserResult::error(HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
return HttpParserResult::error(headOverflowStatus(data, maxFallbackSize), HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
Comment thread
robobun marked this conversation as resolved.
}
}

Expand Down
140 changes: 140 additions & 0 deletions test/js/bun/http/serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3676,3 +3676,143 @@ it.each([

expect(statusLine).toBe("HTTP/1.1 400 Bad Request");
});

describe("parser-tier error responses", () => {
async function rawRequest(port: number, bytes: string | string[]): Promise<string> {
const received: Buffer[] = [];
const { promise, resolve } = Promise.withResolvers<void>();
await using conn = await Bun.connect({
hostname: "127.0.0.1",
port,
socket: {
data(_s, chunk) {
received.push(Buffer.from(chunk));
},
close() {
resolve();
},
end() {
resolve();
},
error() {
resolve();
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
});
const pieces = Array.isArray(bytes) ? bytes : [bytes];
for (let i = 0; i < pieces.length; i++) {
conn.write(pieces[i]);
conn.flush();
// Client and server share one event loop: yield so the server's onData
// runs for this write before the next one is queued, letting the
// array form actually deliver separate reads.
if (i + 1 < pieces.length) await Bun.sleep(0);
}
await promise;
return Buffer.concat(received).toString("latin1");
}

function head(raw: string) {
const h = raw.split("\r\n\r\n")[0];
const lines = h.split("\r\n");
const headers = Object.fromEntries(
lines.slice(1).map(l => {
const i = l.indexOf(":");
return [l.slice(0, i).toLowerCase(), l.slice(i + 1).trim()];
}),
);
return { status: lines[0], headers, body: raw.slice(h.length + 4) };
}

// RFC 9110 6.6.1: an origin server with a clock MUST send Date on every
// response; the parser-rejection path used to write only the status line and
// Connection: close.
it("400/431/505 carry Date and Content-Length: 0", async () => {
using server = serve({ port: 0, hostname: "127.0.0.1", development: false, fetch: () => new Response("ok") });
const bigHeader = Buffer.alloc(65536, "a").toString();
const got: Record<string, ReturnType<typeof head>> = {};
for (const [name, req] of [
["400", "GET / HTTP/1.1\r\nHost: x\r\nNo-Colon-Line\r\n\r\n"],
["431", `GET / HTTP/1.1\r\nHost: x\r\nX-Big: ${bigHeader}\r\n\r\n`],
["505", "GET / HTTP/9.9\r\nHost: x\r\n\r\n"],
] as const) {
got[name] = head(await rawRequest(server.port, req));
}
expect({
400: { status: got["400"].status, cl: got["400"].headers["content-length"], conn: got["400"].headers.connection },
431: { status: got["431"].status, cl: got["431"].headers["content-length"], conn: got["431"].headers.connection },
505: { status: got["505"].status, cl: got["505"].headers["content-length"], conn: got["505"].headers.connection },
}).toEqual({
400: { status: "HTTP/1.1 400 Bad Request", cl: "0", conn: "close" },
431: { status: "HTTP/1.1 431 Request Header Fields Too Large", cl: "0", conn: "close" },
505: { status: "HTTP/1.1 505 HTTP Version Not Supported", cl: "0", conn: "close" },
});
for (const name of ["400", "431", "505"] as const) {
expect(got[name].headers.date).toMatch(/^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/);
expect(got[name].body).toBe("");
}
});

// RFC 9110 15.5.15: a request-target that alone exceeds the server's limit is
// 414; only oversize header fields are 431. The two are distinguished by
// whether the request-line has terminated within the limit.
it("an oversize request-target is 414, oversize header fields stay 431", async () => {
using server = serve({ port: 0, hostname: "127.0.0.1", development: false, fetch: () => new Response("ok") });
const big = Buffer.alloc(65536, "a").toString();
const uriOnce = head(await rawRequest(server.port, `GET /${big} HTTP/1.1\r\nHost: x\r\n\r\n`));
// Same request-line split across reads so the buffered-overflow classifier
// (headOverflowStatus over the fallback buffer) is what returns 414: the
// first two reads land in the fallback buffer, the third fills it.
const trickled = head(
await rawRequest(server.port, [
"GET /",
Buffer.alloc(10000, "a").toString(),
big,
" HTTP/1.1\r\nHost: x\r\n\r\n",
]),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
const hdr = head(await rawRequest(server.port, `GET / HTTP/1.1\r\nHost: x\r\nX-Big: ${big}\r\n\r\n`));
expect({ uriOnce: uriOnce.status, trickled: trickled.status, hdr: hdr.status }).toEqual({
uriOnce: "HTTP/1.1 414 URI Too Long",
trickled: "HTTP/1.1 414 URI Too Long",
hdr: "HTTP/1.1 431 Request Header Fields Too Large",
});
expect(uriOnce.headers.date).toBeTruthy();
expect(uriOnce.headers["content-length"]).toBe("0");
});

// A syntactically valid method token that the router has no handler for used
// to close the connection with zero response bytes. RFC 9110 9.1: an
// unrecognized method SHOULD receive 501. The broad extension set in the
// router's method table still dispatches to fetch().
it("an unknown method token gets 501; router-known extension methods dispatch", async () => {
let dispatched: string[] = [];
using server = serve({
port: 0,
hostname: "127.0.0.1",
development: false,
fetch(req) {
dispatched.push(req.method);
return new Response("ok");
},
});
const statuses: Record<string, string> = {};
for (const m of ["FROB", "get", "BREW", "GETS", "PROPFIND", "PURGE"]) {
const raw = await rawRequest(server.port, `${m} / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n`);
expect(raw.length).toBeGreaterThan(0);
statuses[m] = head(raw).status;
}
expect(statuses).toEqual({
FROB: "HTTP/1.1 501 Not Implemented",
get: "HTTP/1.1 501 Not Implemented",
BREW: "HTTP/1.1 501 Not Implemented",
GETS: "HTTP/1.1 501 Not Implemented",
PROPFIND: "HTTP/1.1 200 OK",
PURGE: "HTTP/1.1 200 OK",
});
expect(dispatched).toEqual(["PROPFIND", "PURGE"]);
const h = head(await rawRequest(server.port, "FROB / HTTP/1.1\r\nHost: x\r\n\r\n"));
expect({ cl: h.headers["content-length"], conn: h.headers.connection }).toEqual({ cl: "0", conn: "close" });
expect(h.headers.date).toBeTruthy();
});
});
Loading