You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Shut down cleanly after refusing an oversized request body #157
web/server.ts refuses a POST body over its limit with HTTP 413 and returns from the for await (const chunk of req) loop with the rest of the body unread. When the unread remainder is large, the server's shutdown does not finish:
server.close() never calls back, so close() in startServer never settles past await step(() => closing).
In production, web/cli.ts then never calls lock.release(), and the later close steps (questions.close, planning close, service.close, which closes the Store) are skipped. The process exits only because nothing else keeps the event loop alive.
A client still sending the body can also get ECONNRESET instead of the 413, so the UI shows a network error instead of "Request too large."
This exists on main (ae9fc3d). Found during the independent review of #146. #146 raises the import limit to 6 MiB + 16 KiB, so through plan import it now takes a much larger file to trigger; any other POST still triggers it just above 16 KiB plus the unread remainder.
node repro.mjs 16385: prints 413 and server closed.
node repro.mjs 2000000: prints 413; server closed never prints.
The same happens with startServer and a POST to /api/action with a 16 KiB + 1 MiB body: 413, then app.close() never settles.
Acceptance
After a 413, the connection is closed or the request is drained or destroyed explicitly, so server.close() settles.
The client receives the 413 response for an oversized body, not a reset, where the transport allows it.
A shutdown regression sends a large overage (well beyond the limit) and asserts that app.close() settles and the Store closes. It fails on the current code.
The existing bounded-body and shutdown behaviour (readingBody, shutdown's request.destroy) is preserved.
Correction to the description above, from work on #160.
On the real server, server.close() does call back after a 413 on a large body, but about 6 s late. That is Node's 5 s keepAliveTimeout plus its 1 s buffer. app.close() then settles normally: the lock is released and the Store closes. The repro's "never prints" came from a 3 s cutoff.
The real defects:
The 413 path. The 413 was sent partway through reading the body. Node does not discard the rest of a body the handler has started reading, so the connection stayed half-read until the keep-alive timeout. A client still sending could also be reset before it read the 413.
Refusals before the body is read. This happens with a 403, 405 or 503 and a large body. Node discards the body, but the connection is busy while it does. Shutdown's idle-connection close skips it, so it was held for about 4–6 s.
#160 fixes both. It reads an oversized body to its end before sending the 413, and it closes the connection on a refusal sent before a declared body is read.
Activity
mchwang commentedon Oct 12, 2026
Correction to the description above, from work on #160.
On the real server,
server.close()does call back after a 413 on a large body, but about 6 s late. That is Node's 5 skeepAliveTimeoutplus its 1 s buffer.app.close()then settles normally: the lock is released and the Store closes. The repro's "never prints" came from a 3 s cutoff.The real defects:
#160 fixes both. It reads an oversized body to its end before sending the 413, and it closes the connection on a refusal sent before a declared body is read.
Read an oversized body before refusing it, and close on early refusals (