Skip to content

fix(server): guard Shutdown against a server that never started - #251

Open
kazz187 wants to merge 1 commit into
mainfrom
worktree-9p2h2e_h2c-deprecated-http-server-protocols
Open

fix(server): guard Shutdown against a server that never started#251
kazz187 wants to merge 1 commit into
mainfrom
worktree-9p2h2e_h2c-deprecated-http-server-protocols

Conversation

@kazz187

@kazz187 kazz187 commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Follow-up to #249, from the review of that change.

Summary

  • Fix a nil-pointer panic introduced by refactor(server): replace deprecated h2c handler with http.Server.Protocols #249. Splitting ListenAndServe moved the s.server assignment after the bind, so a failed bind (a port already in use, for example) returns early and leaves s.server nil. cmd/taskguild-server/run.go cancels its context on that error and then calls Shutdown unconditionally, so what used to be a single logged error became a panic:

    net/http.(*Server).Shutdown(0x0, ...)   server.go:3250
    internal.(*Server).Shutdown(...)        internal/server.go:144
    panic: invalid memory address or nil pointer dereference
    

    Before refactor(server): replace deprecated h2c handler with http.Server.Protocols #249 the *http.Server was assigned before binding, so Shutdown was always safe. Shutdown is now a no-op when the server never started.

  • Add a gRPC trailer regression test over h2c. Trailers are mandatory in the gRPC protocol (grpc-status is sent as one) and are the part most at risk from swapping the HTTP/2 implementation from golang.org/x/net/http2 to the one built into net/http. refactor(server): replace deprecated h2c handler with http.Server.Protocols #249 verified this only by hand. The test hits /grpc.health.v1.Health/Check, which bypasses apiKeyMiddleware and needs no injected services.

  • Add a regression test for Shutdown after a failed listen, covering the panic above.

  • Drive test teardown through Shutdown instead of only closing the listener. This is the path whose semantics changed most when h2c stopped hijacking connections, and it was previously untested; it also reaps the connection goroutines.

  • Extract a newTestServer helper to remove the duplicated nil×16 construction.

Test plan

  • go build ./... and go vet ./internal/
  • go test ./... -count=1 — all packages pass
  • go test ./internal/ -race -count=5 — passes, no flakes
  • go tool golangci-lint run ./internal/ --max-issues-per-linter 0 --max-same-issues 0 — 2 findings, both pre-existing (gosec G112, ST1000); no new findings
  • gofmt -l internal/ — clean
  • Negative checks confirm every assertion is load-bearing:
    • removing the Shutdown nil guard → TestShutdownAfterListenFailure fails with the original panic
    • removing SetUnencryptedHTTP2 → h2c and trailer tests fail, HTTP/1.1 still passes
    • removing SetHTTP1 → only the HTTP/1.1 case fails

Notes

The unsynchronized read/write of Server.server between the Serve goroutine and Shutdown on the main goroutine still exists and is tracked separately. The guard here stops the panic but does not make the field race-free.

Splitting ListenAndServe moved the s.server assignment after the bind, so a
failed bind (a port already in use, for example) now returns early and leaves
s.server nil. cmd/taskguild-server cancels its context on that error and then
calls Shutdown unconditionally, turning what used to be a single logged error
into a nil-pointer panic.

Also add regression tests for the gRPC trailer path over h2c, which is the part
most at risk from swapping the HTTP/2 implementation, and drive test teardown
through Shutdown so the path whose semantics changed with native h2c is
actually exercised.
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