fix(uploads): bound the silence in an upload body with a re-armed read deadline (#56) #63

Merged
mathias merged 3 commits from fix/56-upload-idle-read-deadline into main 2026-10-05 04:59:29 +00:00
3 Commits
Author SHA1 Message Date
mathiasandClaude Opus 5.5 b1887e7594 test(uploads): pin the idle bound an unset App resolves to (#56)
CI / Lint / Test / Vet (pull_request) Successful in 32s
CI / Build & Import (pull_request) Skipped
CI / Deploy via GitOps (pull_request) Skipped
Round-2 gatekeeper finding: production never sets UploadIdleTimeout, so it
always runs the zero-means-default branch, and nothing asserted what that
branch resolved to (`idle = time.Hour` kept the suite green).

Extract the resolution into App.uploadIdleTimeout(), expose it via
export_test.go, and assert (&web.App{}) resolves to exactly 60s, a
negative value too, and a set value is honoured. Verified red against the
time.Hour mutant before implementing.

Rename the socket test to what it observes:
...AnUnsetIdleTimeoutDoesNotCutABodyWithSubSecondGaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:55:04 +02:00
mathiasandClaude Opus 5.5 fd74140855 test(uploads): pin the deployed idle bound and its unavailable warning (#56)
CI / Lint / Test / Vet (pull_request) Successful in 32s
CI / Build & Import (pull_request) Skipped
CI / Deploy via GitOps (pull_request) Skipped
Gatekeeper review of PR #63 (comment 15139):
- the 60 s default is what production runs (cmd/tapir never sets
  UploadIdleTimeout) and no test covered it; 1 ns, 60 ms or 24 h all left
  the suite green. Pinned via export_test.go, plus a socket test with the
  timeout unset that accepts a body with 100 ms gaps (60 ms mutant red).
- the "upload idle bound unavailable" warning, the only signal that the
  control did not apply, is now asserted (removing it goes red).
- the deadline clear on io.EOF was redundant (net/http resets the
  deadline before its background read) and is removed; ADR-033 text
  corrected (the part can end before the body, so "cleared on clean
  EOF" did not hold).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:10:51 +02:00
mathiasandClaude Opus 5.5 b106b0dab3 fix(uploads): bound the silence in an upload body with a re-armed read deadline
CI / Lint / Test / Vet (pull_request) Successful in 35s
CI / Build & Import (pull_request) Skipped
CI / Deploy via GitOps (pull_request) Skipped
A body that stopped arriving held one of the receive slots (#47) and a
spool file indefinitely. Every read of the upload body now pushes the
connection read deadline UploadIdleTimeout (default 60 s) ahead, so a
stalled body fails its read, Accept returns and the slot is released,
while a slow but moving body is never cut (ReadTimeout would cut it).

Two things found by running it over a socket:
- metrics.statusWriter had no Unwrap, so http.ResponseController could
  not reach the connection and the bound silently did not apply in the
  production handler stack. It now unwraps; a writer that still cannot
  set a deadline logs "upload idle bound unavailable".
- clearing the deadline on any read error left net/http's pre-reply
  drain of the unread remainder unbounded, so the connection hung. The
  deadline is now cleared only on a clean io.EOF.

ADR-033 limitation amended. A 1-byte-per-59-s drip is still unbounded.

Closes #56

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:57:33 +02:00