Repository navigation
Read an oversized body before refusing it, and close on early refusals (#157) - #160
Merged
Merged
Conversation
A POST over its body limit got its 413 while the client was still sending. The half-read connection was held until Node's keep-alive timeout (about 6 s), so a shutdown started then waited that long, and a client still sending could be reset before it read the reply. Past the limit the server now discards the rest of the body without keeping it and replies 413 once it ends. Shutdown still destroys a request that is discarding its body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…review) A 403 or 405 sent before a large body was read held the connection until the keep-alive timeout too: Node discards only an unread body that fits its socket buffers. Every reply sent while the request body is unread now carries Connection: close. Tests wait for the handler before shutdown, cover an oversized body that completes during the drain, and pin the keep-alive of requests whose body was read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
req.complete is still false while the handler starts, even for a request with no body, so most GET replies lost keep-alive. A reply now closes the connection only when the request declares a body that has not been read to its end. The timing tests use a drain limit above the keep-alive timeout, wait for the server to read past the limit, and keep-alive is pinned for static files, synchronous and asynchronous reads, 404s and a POST whose body was read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The implementation addresses the reported lifecycle issue with comprehensive regression coverage and no unresolved correctness concerns.
0 open findings
What changed in this PR
Ensures HTTP shutdown completes promptly after large-body refusals while preserving bounded memory and keep-alive behavior.
Changes:
- Drains oversized bodies before returning 413.
- Closes connections for early refusals with unread bodies.
- Adds shutdown regressions and lifecycle documentation.
| File | Description |
|---|---|
web/server.ts |
Implements body draining and conditional connection closure. |
test/runner-shutdown.test.ts |
Covers oversized bodies, refusals, shutdown, and keep-alive. |
docs/plan-format.md |
Documents oversized import handling. |
docs/implementation/runner-lifecycle.md |
Updates shutdown lifecycle and regression scenarios. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This was referenced Oct 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #157.
What actually happened
#157 says shutdown never settles after a 413 on a large body. On the real server it does settle, but about 6 s late (Node's 5 s
keepAliveTimeoutplus its 1 s buffer). The lock is released. The original repro used a 3 s cutoff. Two mechanisms caused the delay:What changed
web/server.ts:readingBodystays set.head()helper. When a request declares a body (Content-Length> 0 orTransfer-Encoding) that has not been read to its end,head()sendsConnection: close. Every other reply keeps the connection open.req.completecould not serve as the test: it is false while the handler starts, even for a GET with no body.web/public/app.js,planImportRetries).docs/implementation/runner-lifecycle.md. The 413 sentence indocs/plan-format.md.Tests
The
an oversized request body (#157)block intest/runner-shutdown.test.tshas 15 tests. Its timing tests use a 10 s drain limit, because the suite's default of 2 s masked the 6 s hold. The tests cover:fetch, which keeps the connection the way a browser does. The Store is closed afterwards./, each with both a chunked and aContent-Length4 MiB body: the connection closes and shutdown is prompt.Mutation results (reverting one part each):
web/server.tsConnection: closedeclaresBody=Transfer-EncodingonlyContent-Lengthrefusal casesdeclaresBody=Content-Lengthonly!req.completeinsteadConnection: closeValidation
tsc --noEmit)1ebf2cbrunner-shutdown,runner-planning-feedback)1ebf2cb1ebf2cb(load average about 278)agent-isolation.ymlpaths not touched)Review record
Every independent round used a fresh agent on the full diff against the base shown.
a764e19/7a48a48requestTimeoutbounds the discard. A probe showed no 408 within 60 s.a764e19/7a48a48head()withConnection: close; chunks cleared past the limit; tests reworked (86cab51)a764e19/86cab51req.completeclosed most GET replies. Timing tests could not fail under the 2 s drain. Drain test ordering unchecked. Load fragility.declaresBody && !req.readableEnded; 10 s drain for timing tests;serverReadprecondition; more keep-alive cases (5d4aac5). Mergedorigin/main04a571404a5714/694c36d{ timeout: 30_000 }; drain case timed (0dc4f9a)04a5714/0dc4f9a5de9bea)04a5714/5de9bea2c91b6f)04a5714/2c91b6fContent-Length, the browser's shape. Dropping that half of the check passed every test.Content-Lengthvariants; Store-closed assertions (1ebf2cb)04a5714/1ebf2cbhead()and needs a narrow race (a body request on a live keep-alive connection after shutdown starts)04a5714/1ebf2cbKnown and accepted:
Connection: close.86cab51's message states an earlier, wrong mechanism ("discards only an unread body that fits its socket buffers"). This PR body is the corrected account.Review-lesson audit
requestTimeoutclaim; round 2 "not kept"; rounds 5–6 comment accuracy: covered by CLAUDE.md "Review readiness", "Treat every behavioural claim … as something to verify".Content-Lengthbodies.req.completemisuse: one-off Node API semantics. It is recorded in the code comment.AGENTS.md/CLAUDE.mdupdate needed.Out of scope, noticed
readFileSync. A read failure would make the catch path set headers again and throw out of the async handler. Pre-existing on main.🤖 Generated with Claude Code