server: set ReadTimeout, WriteTimeout, and IdleTimeout (closes #99)
All checks were successful
check / check (push) Successful in 37s
All checks were successful
check / check (push) Successful in 37s
The http.Server literal only set ReadHeaderTimeout. The other three timeouts defaulted to zero, which in net/http means no limit: past the header phase a peer could hold a connection open forever, responses had no write deadline, and keep-alive connections were never reaped. REPO_POLICIES.md requires all four before 1.0. All four are now named constants in internal/server/server.go: ReadHeaderTimeout 10s (unchanged) ReadTimeout 15s whole request; every route is a bodyless GET WriteTimeout 75s handler execution plus response flush IdleTimeout 120s keep-alive reaping WriteTimeout must exceed the 60s chimw.Timeout handler budget in routes.go. net/http arms the write deadline once the request headers have been read, so it covers handler execution as well as the response write; a smaller value would sever the connection before a handler that legitimately used its full budget could respond, making that budget unreachable. The 15s difference is the response-flush allowance. The comment on the const block states this relationship. IdleTimeout sits above the common Prometheus scrape intervals so the scraper reuses its connection instead of reconnecting each cycle, while an abandoned connection is still reaped within two minutes. The http.Server literal moved into newHTTPServer so the configuration is testable without binding a socket. Tests assert all four fields are non-zero, that WriteTimeout exceeds the handler budget, and that ReadTimeout covers ReadHeaderTimeout; they compare configured values only and measure no elapsed time, so they cannot flake.
This commit is contained in:
@@ -33,8 +33,52 @@ type Params struct {
|
||||
// shutdownTimeout is how long to wait for graceful shutdown.
|
||||
const shutdownTimeout = 30 * time.Second
|
||||
|
||||
// readHeaderTimeout is the max duration for reading request headers.
|
||||
const readHeaderTimeout = 10 * time.Second
|
||||
// Socket-level timeouts for the HTTP server.
|
||||
//
|
||||
// These bound time spent on the connection itself and are a distinct
|
||||
// control from the per-request handler budget enforced by
|
||||
// chimw.Timeout(requestTimeout) in routes.go: that one cancels the
|
||||
// request context after requestTimeout but never touches the socket,
|
||||
// so without the values below a peer can hold a connection open
|
||||
// forever (slowloris, unreaped keep-alives).
|
||||
//
|
||||
// The one hard constraint between the two controls is
|
||||
// writeTimeout > requestTimeout. net/http arms the write deadline
|
||||
// once the request headers have been read, so on a plaintext
|
||||
// connection it covers handler execution AND the response flush. If
|
||||
// writeTimeout were <= requestTimeout the server would sever the
|
||||
// connection before a handler that legitimately consumed its full
|
||||
// budget could emit anything, making the 60s budget unreachable in
|
||||
// practice. The margin between them is the response-flush allowance.
|
||||
//
|
||||
// The only clients of this service are browsers loading the dashboard
|
||||
// and a Prometheus scraper; the values are sized for those.
|
||||
const (
|
||||
// readHeaderTimeout is the max duration for reading request
|
||||
// headers.
|
||||
readHeaderTimeout = 10 * time.Second
|
||||
|
||||
// readTimeout bounds reading the entire request, headers plus
|
||||
// body. Every route here is a GET with no body, so this only
|
||||
// ever needs to cover headers; the extra 5s over
|
||||
// readHeaderTimeout is slack, not a real allowance, and keeps a
|
||||
// body dribbled one byte at a time from holding the read side
|
||||
// open indefinitely.
|
||||
readTimeout = 15 * time.Second
|
||||
|
||||
// writeTimeout must exceed the requestTimeout handler budget
|
||||
// (60s) per the note above. The 15s difference is the allowance
|
||||
// for flushing a completed response to a slow client.
|
||||
writeTimeout = 75 * time.Second
|
||||
|
||||
// idleTimeout reaps keep-alive connections between requests. It
|
||||
// is deliberately longer than the common Prometheus scrape
|
||||
// intervals (15s/30s/60s) so the scraper reuses its connection
|
||||
// rather than reconnecting every cycle, while a browser tab
|
||||
// left open on the dashboard stops occupying a connection
|
||||
// within two minutes of going quiet.
|
||||
idleTimeout = 120 * time.Second
|
||||
)
|
||||
|
||||
// Server is the HTTP server.
|
||||
type Server struct {
|
||||
@@ -76,16 +120,29 @@ func New(
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// newHTTPServer builds the listening http.Server with every
|
||||
// socket-level timeout set. All four are set deliberately: a zero
|
||||
// value in net/http means "no limit", not "some default".
|
||||
func newHTTPServer(
|
||||
listenAddr string,
|
||||
handler http.Handler,
|
||||
) *http.Server {
|
||||
return &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: handler,
|
||||
ReadTimeout: readTimeout,
|
||||
ReadHeaderTimeout: readHeaderTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
IdleTimeout: idleTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the HTTP server.
|
||||
func (s *Server) Run() {
|
||||
s.SetupRoutes()
|
||||
|
||||
listenAddr := fmt.Sprintf(":%d", s.port)
|
||||
s.httpServer = &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: s,
|
||||
ReadHeaderTimeout: readHeaderTimeout,
|
||||
}
|
||||
s.httpServer = newHTTPServer(listenAddr, s)
|
||||
|
||||
s.log.Info("http server starting", "addr", listenAddr)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user