server: set all four http.Server socket timeouts (#118)
check / check (push) Successful in 3m50s
check / check (push) Successful in 3m50s
The http.Server was built with only ReadHeaderTimeout set; the other three timeouts were zero, which in net/http means no limit, so a peer could hold a connection open past the header phase, responses had no write deadline, and keep-alive connections were never reaped. ReadTimeout 15s, WriteTimeout 75s, and IdleTimeout 120s now join ReadHeaderTimeout 10s as named constants. WriteTimeout must stay above the 60s chimw.Timeout handler budget, because net/http arms the write deadline once request headers are read; a test fails the build if either number moves alone. The server literal moved into newHTTPServer so the configuration can be asserted without binding a socket (closes #99) Model: opus-5
This commit was merged in pull request #118.
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