Intent
Harden the harnessd HTTP server against slowloris/body-bomb DoS by setting full timeouts and MaxHeaderBytes, wrapping non-streaming handlers in http.TimeoutHandler, and adding http.MaxBytesReader to every request body.
Plan reference
Source finding
harnessd http.Server sets only ReadHeaderTimeout (cmd/harnessd/runtime_container.go:159-163). No ReadTimeout, IdleTimeout, MaxHeaderBytes, or http.TimeoutHandler on non-streaming handlers. Slowloris body reads and accumulated idle keep-alive conns pin FDs unbounded. Also: no http.MaxBytesReader on any request body — every json.NewDecoder(r.Body).Decode site in internal/server/; a 50 GB body OOMs the daemon.
Scope
- Allowed files (test):
internal/server/http_server_hardening_test.go (new), internal/server/http_test.go (extend)
- Allowed files (impl):
cmd/harnessd/runtime_container.go, internal/server/http.go, internal/server/auth.go
- Dependencies: none
- Complexity: low — Risk: low (must ensure SSE/streaming handlers are NOT wrapped by
TimeoutHandler)
Acceptance criteria
Use slice T13 in the plan as detailed acceptance criteria. Summary:
- Failing tests written first, compile and fail on
main.
http.Server is constructed with non-zero ReadTimeout, IdleTimeout, MaxHeaderBytes; non-streaming handlers are wrapped in http.TimeoutHandler (introspect via reflect on the chain, or assert a body bigger than the limit triggers http.MaxBytesError).
- Sending a 5 MiB JSON body to
POST /v1/runs returns 413 (or the MaxBytesReader status) and does not consume more than ~1.1 MiB of memory.
- An SSE handler that respects
r.Context().Done() is NOT wrapped by TimeoutHandler (assert a long-lived SSE stream survives a 60s TimeoutHandler boundary).
- Impl: set
ReadTimeout: 60s, IdleTimeout: 120s, MaxHeaderBytes: 1 << 20 on the http.Server. Add an authAndLimit middleware that wraps r.Body in http.MaxBytesReader(w, r.Body, maxBodyBytes) (default 4 MiB for replay, 1 MiB otherwise; per-route override). Wrap non-streaming handlers (everything whose handler name does NOT contain Events/Stream/Wait) in http.TimeoutHandler(h, 30*time.Second, {"error":{"code":"timeout"}}). Streaming handlers keep their own r.Context().Done() discipline.
go test ./internal/server/... ./cmd/harnessd/... -race green; ./scripts/test-regression.sh green.
Guardrails
- Strict TDD; failing regression test must compile and fail on
main first.
- Worktree flow; own branch, merge via
verify-and-merge.sh.
- Update
.context/harness-reliability/tracker.md on pick-up and merge.
- Deterministic tests only —
httptest.Server, no real FD exhaustion.
- Append engineering-log entry on merge.
Intent
Harden the
harnessdHTTP server against slowloris/body-bomb DoS by setting full timeouts andMaxHeaderBytes, wrapping non-streaming handlers inhttp.TimeoutHandler, and addinghttp.MaxBytesReaderto every request body.Plan reference
docs/plans/2026-06-24-harness-reliability-plan.md— slice T13Source finding
harnessdhttp.Serversets onlyReadHeaderTimeout(cmd/harnessd/runtime_container.go:159-163). NoReadTimeout,IdleTimeout,MaxHeaderBytes, orhttp.TimeoutHandleron non-streaming handlers. Slowloris body reads and accumulated idle keep-alive conns pin FDs unbounded. Also: nohttp.MaxBytesReaderon any request body — everyjson.NewDecoder(r.Body).Decodesite ininternal/server/; a 50 GB body OOMs the daemon.Scope
internal/server/http_server_hardening_test.go(new),internal/server/http_test.go(extend)cmd/harnessd/runtime_container.go,internal/server/http.go,internal/server/auth.goTimeoutHandler)Acceptance criteria
Use slice T13 in the plan as detailed acceptance criteria. Summary:
main.http.Serveris constructed with non-zeroReadTimeout,IdleTimeout,MaxHeaderBytes; non-streaming handlers are wrapped inhttp.TimeoutHandler(introspect via reflect on the chain, or assert a body bigger than the limit triggershttp.MaxBytesError).POST /v1/runsreturns 413 (or theMaxBytesReaderstatus) and does not consume more than ~1.1 MiB of memory.r.Context().Done()is NOT wrapped byTimeoutHandler(assert a long-lived SSE stream survives a 60sTimeoutHandlerboundary).ReadTimeout: 60s,IdleTimeout: 120s,MaxHeaderBytes: 1 << 20on thehttp.Server. Add anauthAndLimitmiddleware that wrapsr.Bodyinhttp.MaxBytesReader(w, r.Body, maxBodyBytes)(default 4 MiB for replay, 1 MiB otherwise; per-route override). Wrap non-streaming handlers (everything whose handler name does NOT containEvents/Stream/Wait) inhttp.TimeoutHandler(h, 30*time.Second,{"error":{"code":"timeout"}}). Streaming handlers keep their ownr.Context().Done()discipline.go test ./internal/server/... ./cmd/harnessd/... -racegreen;./scripts/test-regression.shgreen.Guardrails
mainfirst.verify-and-merge.sh..context/harness-reliability/tracker.mdon pick-up and merge.httptest.Server, no real FD exhaustion.