REPO_POLICIES.md requires, before tagging 1.0, that HTTP services set ReadTimeout, ReadHeaderTimeout, WriteTimeout, and IdleTimeout on the http.Server. Only one of the four is set.
Current state (audited against origin/main, commit 9347a28)
readHeaderTimeout is 10 * time.Second (internal/server/server.go:36-37). ReadTimeout, WriteTimeout, and IdleTimeout are 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) at internal/server/routes.go:14-15,26 bounds 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.go sets all four fields on the http.Server literal: ReadTimeout, ReadHeaderTimeout, WriteTimeout, IdleTimeout.
Each duration is a named const in the same file alongside the existing readHeaderTimeout, so the values are self-documenting rather than inline magic numbers.
The values are internally consistent with the existing 60s chimw.Timeout handler budget — WriteTimeout must 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.
A test in internal/server asserts that the constructed http.Server has all four fields set to non-zero values, so a future refactor cannot silently drop one.
README documents the timeout values if it documents server tunables; if they are not configurable, no env-var documentation is needed.
make check is green, and TODO.md is 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.
`REPO_POLICIES.md` requires, before tagging 1.0, that HTTP services set `ReadTimeout`, `ReadHeaderTimeout`, `WriteTimeout`, and `IdleTimeout` on the `http.Server`. Only one of the four is set.
## Current state (audited against `origin/main`, commit `9347a28`)
`internal/server/server.go:84-88`:
```go
s.httpServer = &http.Server{
Addr: listenAddr,
Handler: s,
ReadHeaderTimeout: readHeaderTimeout,
}
```
`readHeaderTimeout` is `10 * time.Second` (`internal/server/server.go:36-37`). `ReadTimeout`, `WriteTimeout`, and `IdleTimeout` are 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)` at `internal/server/routes.go:14-15,26` bounds *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
1. `internal/server/server.go` sets all four fields on the `http.Server` literal: `ReadTimeout`, `ReadHeaderTimeout`, `WriteTimeout`, `IdleTimeout`.
2. Each duration is a named `const` in the same file alongside the existing `readHeaderTimeout`, so the values are self-documenting rather than inline magic numbers.
3. The values are internally consistent with the existing 60s `chimw.Timeout` handler budget — `WriteTimeout` must 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.
4. A test in `internal/server` asserts that the constructed `http.Server` has all four fields set to non-zero values, so a future refactor cannot silently drop one.
5. README documents the timeout values if it documents server tunables; if they are not configurable, no env-var documentation is needed.
6. `make check` is green, and `TODO.md` is 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.
clawbot
added this to the 1.0 milestone 2026-08-09 03:36:09 +02:00
Branch fix/99-server-timeouts off origin/main (9347a28). All code changes confined to internal/server.
1. Constants in internal/server/server.go
Four named constants next to the existing shutdownTimeout, with a comment block explaining the relationship to the 60s chimw.Timeout(requestTimeout) handler budget in routes.go:
readHeaderTimeout = 10 * time.Second — unchanged.
readTimeout = 15 * time.Second — total header+body read deadline. Every route in this service is a GET with 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. In net/http the 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 full chimw.Timeout budget 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.Server literal into a small unexported newHTTPServer(listenAddr string, handler http.Handler) *http.Server in server.go, called from Run(). This keeps the literal in server.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 existing export_test.go convention in internal/handlers and internal/notify) exporting newHTTPServer and the requestTimeout value, plus internal/server/server_test.go in package server_test asserting:
all four fields are non-zero (DoD 4);
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.md updated 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 to routes.go, internal/middleware, internal/watcher, internal/resolver, or .golangci.yml.
Verification
make check (green before PR), GOFLAGS=-count=1 make test run 10x consecutively under -race, and docker build --no-cache . to force a real script/cibuild run rather than a cached layer (#115).
## Implementation plan
Branch `fix/99-server-timeouts` off `origin/main` (`9347a28`). All code changes confined to `internal/server`.
### 1. Constants in `internal/server/server.go`
Four named constants next to the existing `shutdownTimeout`, with a comment block explaining the relationship to the 60s `chimw.Timeout(requestTimeout)` handler budget in `routes.go`:
- `readHeaderTimeout = 10 * time.Second` — unchanged.
- `readTimeout = 15 * time.Second` — total header+body read deadline. Every route in this service is a `GET` with 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.** In `net/http` the 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 full `chimw.Timeout` budget 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.Server` literal into a small unexported `newHTTPServer(listenAddr string, handler http.Handler) *http.Server` in `server.go`, called from `Run()`. This keeps the literal in `server.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 existing `export_test.go` convention in `internal/handlers` and `internal/notify`) exporting `newHTTPServer` and the `requestTimeout` value, plus `internal/server/server_test.go` in `package server_test` asserting:
- all four fields are non-zero (DoD 4);
- `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.md` updated 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 to `routes.go`, `internal/middleware`, `internal/watcher`, `internal/resolver`, or `.golangci.yml`.
### Verification
`make check` (green before PR), `GOFLAGS=-count=1 make test` run 10x consecutively under `-race`, and `docker build --no-cache .` to force a real `script/cibuild` run rather than a cached layer (#115).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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).clawbot referenced this issue2026-09-03 19:07:02 +02:00
clawbot referenced this issue2026-09-04 00:10:40 +02:00
clawbot referenced this issue2026-09-04 01:19:43 +02:00
clawbot referenced this issue2026-09-04 01:21:31 +02:00