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.
113 lines
2.9 KiB
Go
113 lines
2.9 KiB
Go
package server_test
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/server"
|
|
)
|
|
|
|
// noopHandler stands in for the router; newHTTPServer only stores it.
|
|
func noopHandler() http.Handler {
|
|
return http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
},
|
|
)
|
|
}
|
|
|
|
// TestHTTPServerTimeoutsAreSet asserts that every socket-level
|
|
// timeout is configured. A zero value in net/http means "no limit",
|
|
// so a refactor that silently drops one of these reintroduces the
|
|
// slowloris / unreaped-keep-alive exposure this guards against.
|
|
//
|
|
// The assertions are on the configured field values only; nothing
|
|
// here measures elapsed time, so the test cannot flake on timing.
|
|
func TestHTTPServerTimeoutsAreSet(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := server.NewHTTPServer(":8080", noopHandler())
|
|
|
|
if srv.ReadTimeout <= 0 {
|
|
t.Errorf(
|
|
"ReadTimeout must be non-zero, got %v",
|
|
srv.ReadTimeout,
|
|
)
|
|
}
|
|
|
|
if srv.ReadHeaderTimeout <= 0 {
|
|
t.Errorf(
|
|
"ReadHeaderTimeout must be non-zero, got %v",
|
|
srv.ReadHeaderTimeout,
|
|
)
|
|
}
|
|
|
|
if srv.WriteTimeout <= 0 {
|
|
t.Errorf(
|
|
"WriteTimeout must be non-zero, got %v",
|
|
srv.WriteTimeout,
|
|
)
|
|
}
|
|
|
|
if srv.IdleTimeout <= 0 {
|
|
t.Errorf(
|
|
"IdleTimeout must be non-zero, got %v",
|
|
srv.IdleTimeout,
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestWriteTimeoutExceedsHandlerBudget pins the one relationship the
|
|
// values must satisfy. net/http arms the write deadline once request
|
|
// headers are read, so it covers handler execution plus the response
|
|
// flush. If WriteTimeout were not greater than the chimw.Timeout
|
|
// handler budget, the connection would be severed before a handler
|
|
// that used its full budget could respond, making that budget
|
|
// unreachable.
|
|
func TestWriteTimeoutExceedsHandlerBudget(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := server.NewHTTPServer(":8080", noopHandler())
|
|
|
|
if srv.WriteTimeout <= server.RequestTimeout {
|
|
t.Errorf(
|
|
"WriteTimeout (%v) must exceed handler budget (%v)",
|
|
srv.WriteTimeout,
|
|
server.RequestTimeout,
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestReadTimeoutCoversHeaderTimeout asserts the read deadline for
|
|
// the whole request is at least as long as the header-only deadline;
|
|
// a smaller ReadTimeout would make ReadHeaderTimeout unreachable.
|
|
func TestReadTimeoutCoversHeaderTimeout(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := server.NewHTTPServer(":8080", noopHandler())
|
|
|
|
if srv.ReadTimeout < srv.ReadHeaderTimeout {
|
|
t.Errorf(
|
|
"ReadTimeout (%v) must be >= ReadHeaderTimeout (%v)",
|
|
srv.ReadTimeout,
|
|
srv.ReadHeaderTimeout,
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestHTTPServerAddrAndHandler covers the rest of the constructor so
|
|
// a future edit cannot drop the listen address or the handler.
|
|
func TestHTTPServerAddrAndHandler(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := server.NewHTTPServer(":9999", noopHandler())
|
|
|
|
if srv.Addr != ":9999" {
|
|
t.Errorf("Addr = %q, want %q", srv.Addr, ":9999")
|
|
}
|
|
|
|
if srv.Handler == nil {
|
|
t.Error("Handler must not be nil")
|
|
}
|
|
}
|