Public: anyone can read this; the crew can change it.
Security: a request body has a cap but no deadline — a trickling client holds a handler open #87
Check · Review · Land · Live done — Landed and live in production.
`readBody` in `src/http.ts` now has a clock as well as a cap. One wall-clock budget — `BODY_MS`, five seconds — covers the whole body, raced against every `reader.read()` and cleared however the read ends, so a client that trickles a byte at a time can no longer hold a handler open; the drain loop after an oversized body is inside the same clock. Out of time is the end of the request: the stream i…
shipped production is serving it deploy ab962da
Sign in to join this conversation.
readBodyinsrc/http.tsnow has a clock as well as a cap. One wall-clock budget —BODY_MS, five seconds — covers the whole body, raced against everyreader.read()and cleared however the read ends, so a client that trickles a byte at a time can no longer hold a handler open; the drain loop after an oversized body is inside the same clock. Out of time is the end of the request: the stream is let go (the cancel is not awaited, since a stream that stalled is exactly the one that would stall there too) and the read returns the status that refuses it —408, or413when the body was already past the cap.The refusal used to be
undefinedfromreadBody/readText/readForm; it is now the status itself (Refused = 413 | 408, withisRefusedandrefusalfor the message), which let the two duplicated "that body was too large" replies insrc/server.tsbecome onerefuse()that sends the right status. That response carriesConnection: close: bytes of the abandoned body are still on the wire, and without it the next request on that connection reads them as its own — which is exactly what the first draft of the end-to-end test walked into.Worth a look:
src/http.ts:41-131for the clock and the two loops, andsrc/server.ts:92-101for the refusal. Tests are insrc/server.test.tsbeside the cap's own — the deadline unit-tested with a short budget (a stream that stalls, one that never sends a first byte, one that stalls mid-drain, one that finishes in time), plus an end-to-end case written on a raw socket, becausefetchcannot say "and now nothing, forever": half a form body and silence comes back408with the security floor on it, in about five seconds.bun testis 136 pass / 0 fail, up from 131, and the suite is ~6s slower for that one real stall.What I did not do: make the budget configurable per route, touch Bun's
idleTimeout, or add a limit on concurrent in-flight bodies — the deadline is the other half of the cap the issue names, and that is all this changes. Bun does not close the socket for us when the handler setsConnection: close, so an abusive client that ignores the header still holds an idle socket until the server's 60s idle timeout; it holds no handler, which was the hole. Separately,bun test --randomizeturned up a pre-existing order dependency in the audit paging test (it pages through rows other tests left behind) — filed as issue #86, untouched here.🤖 Generated with Claude Code
Closes #54