Skip to content

feat: add configurable http.Server timeouts and statusWriter.Unwrap#110

Open
coolbit wants to merge 4 commits into
masterfrom
add-timeout-to-http-server
Open

feat: add configurable http.Server timeouts and statusWriter.Unwrap#110
coolbit wants to merge 4 commits into
masterfrom
add-timeout-to-http-server

Conversation

@coolbit

@coolbit coolbit commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Why

The http.Server built by server.newServer set none of ReadTimeout,
ReadHeaderTimeout, WriteTimeout or IdleTimeout. A slow or stalled client
— e.g. one that sends request headers but never sends the body — holds a
handler goroutine until the upstream idle timeout (ALB, 60s) closes the
connection. We hit this in production on a page-server API.

Separately, contexts.statusWriter (installed by WithHTTPStatus, part of
DefaultMiddleware) embedded http.ResponseWriter without an Unwrap, so
http.ResponseController could not reach the underlying net.Conn
per-request SetReadDeadline/SetWriteDeadline silently failed. This blocked
the natural per-route fix.

What

  • server.Config: add ReadTimeout, ReadHeaderTimeout, WriteTimeout,
    IdleTimeout and map them onto the http.Server.
  • service.ListenAndServe: read these from SERVER_* env vars, log the
    effective values at startup. Invalid or negative values are logged and
    ignored (a negative deadline would make every request fail immediately).
  • contexts.statusWriter: add Unwrap() so ResponseController deadlines
    work through the wrapper. Existing Flush() is kept.

Backward compatibility

Fully backward compatible — nothing changes for current users:

  • Timeouts are opt-in. Unset env var ⇒ 0 ⇒ disabled ⇒ identical to
    today's behaviour. Only services that set SERVER_* get timeouts.
  • Unwrap() is purely additive. It does not change type assertions
    (w.(http.Flusher) / w.(http.Hijacker) are unaffected), and Flush() is
    retained so existing SSE callers keep working.

How to use

Set on the service (e.g. ECS task definition):

SERVER_READ_HEADER_TIMEOUT=10s
SERVER_READ_TIMEOUT=30s
SERVER_WRITE_TIMEOUT=60s # must exceed the slowest legitimate handler
SERVER_IDLE_TIMEOUT=120s # keep larger than the A

Per-route overrides are now possible via
http.NewResponseController(w).SetReadDeadline(...) inside a route-scoped
middleware.

Testing

go test ./server/ ./service/ ./contexts/ — added
SERVER_* env parsing (incl. invalid/negative/zero), the env-to-field wiring,
Unwrap, ResponseController reaching the connect
and a Flusher regression guard.

coolbit added 4 commits July 8, 2026 13:37
statusWriter embedded http.ResponseWriter but exposed no Unwrap, so
http.ResponseController could not reach the underlying net.Conn:
per-request SetReadDeadline/SetWriteDeadline silently failed once
WithHTTPStatus wrapped a handler. Add Unwrap so response-controller
deadlines work through the wrapper. Flush is kept for existing SSE
callers that type-assert http.Flusher directly.
A negative duration parses fine but would make every request time out
immediately, so treat it like an invalid value (log and disable).
Extract the env-to-Config wiring into serverTimeouts so the mapping is
unit tested, guarding against a mismapped field. Add cases for negative
and explicit-zero durations.
@coolbit coolbit changed the title feat: add configurable http.Server timeouts feat: add configurable http.Server timeouts and statusWriter.Unwrap Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant