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
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")
|
|
}
|
|
}
|