http.Server is missing ReadTimeout, WriteTimeout, and IdleTimeout #99
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
REPO_POLICIES.mdrequires, before tagging 1.0, that HTTP services setReadTimeout,ReadHeaderTimeout,WriteTimeout, andIdleTimeouton thehttp.Server. Only one of the four is set.Current state (audited against
origin/main, commit9347a28)internal/server/server.go:84-88:readHeaderTimeoutis10 * time.Second(internal/server/server.go:36-37).ReadTimeout,WriteTimeout, andIdleTimeoutare all absent, so they default to zero — meaning no limit.Consequences: a client can hold a connection open indefinitely once it is past the header phase, responses have no write deadline, and keep-alive connections are never reaped. This is straightforward slowloris / idle-connection resource exhaustion on a service that is intended to be exposed to the internet.
Note the global
chimw.Timeout(60 * time.Second)atinternal/server/routes.go:14-15,26bounds handler execution, which is a different control — it does not bound socket-level read/write/idle time and does not close the connection.Definition of done
internal/server/server.gosets all four fields on thehttp.Serverliteral:ReadTimeout,ReadHeaderTimeout,WriteTimeout,IdleTimeout.constin the same file alongside the existingreadHeaderTimeout, so the values are self-documenting rather than inline magic numbers.chimw.Timeouthandler budget —WriteTimeoutmust be greater than the handler timeout, otherwise the server severs the connection before a legitimately slow handler can finish and the 60s budget becomes unreachable. State the chosen relationship in a comment.internal/serverasserts that the constructedhttp.Serverhas all four fields set to non-zero values, so a future refactor cannot silently drop one.make checkis green, andTODO.mdis updated in the same commit as the work.The finishing commit's title must end with
(closes #N)referencing this issue.Out of scope
Security response headers (#98), rate limiting, and CORS scoping are tracked separately — do not fold them into this PR.
Implementation plan
Branch
fix/99-server-timeoutsofforigin/main(9347a28). All code changes confined tointernal/server.1. Constants in
internal/server/server.goFour named constants next to the existing
shutdownTimeout, with a comment block explaining the relationship to the 60schimw.Timeout(requestTimeout)handler budget inroutes.go:readHeaderTimeout = 10 * time.Second— unchanged.readTimeout = 15 * time.Second— total header+body read deadline. Every route in this service is aGETwith no request body, so 15s is already far beyond what any legitimate client needs; it exists purely to bound a slowloris body dribble. Must be >=readHeaderTimeout, and 5s of headroom past it is plenty.writeTimeout = 75 * time.Second— must exceed the 60s handler budget. Innet/httpthe write deadline for a plaintext connection is set once the request headers are read, so it covers handler execution and the response flush. If it were <= 60s the socket would be severed before a handler that legitimately used its fullchimw.Timeoutbudget could emit a response, making that budget unreachable. 60s + 15s of flush headroom.idleTimeout = 120 * time.Second— keep-alive reaping. The only clients are browsers on the dashboard and a Prometheus scraper; 120s sits above the common scrape intervals (15s/30s/60s) so the scraper's connection is reused rather than re-handshaked every cycle, while an abandoned connection is still reaped inside two minutes.2. Construction
Extract the
http.Serverliteral into a small unexportednewHTTPServer(listenAddr string, handler http.Handler) *http.Serverinserver.go, called fromRun(). This keeps the literal inserver.go(DoD 1) while making the configured value testable without binding a socket.3. Test
New
internal/server/export_test.go(matching the repo's existingexport_test.goconvention ininternal/handlersandinternal/notify) exportingnewHTTPServerand therequestTimeoutvalue, plusinternal/server/server_test.goinpackage server_testasserting:WriteTimeout > requestTimeout, so a future edit to either number cannot silently break the invariant;ReadTimeout >= ReadHeaderTimeout.These assert on configured field values only — no timing/duration behaviour is measured, so the test cannot flake.
4. Docs
The timeouts are compile-time constants, not env vars, so per DoD 5 nothing goes in the README env-var table. I will add a one-line note in the README's HTTP/architecture prose stating the four socket timeouts and the handler-budget relationship, so the values are discoverable.
TODO.mdupdated in the same commit as the work.Out of scope, not touched
Security headers (#98 / PR #112), rate limiting (#100),
http.MaxBytesReader(#101), CORS scoping. No changes toroutes.go,internal/middleware,internal/watcher,internal/resolver, or.golangci.yml.Verification
make check(green before PR),GOFLAGS=-count=1 make testrun 10x consecutively under-race, anddocker build --no-cache .to force a realscript/cibuildrun rather than a cached layer (#115).