openlang

httptest Server.Close hangs, and no failure message from my test

Is it possible that httptest.Server.Close hangs on Go 1.23 with keep-alive? I run go test -timeout 5s ./... and the new test neither passes nor fails. It sits until the timeout, then I get panic: test timed out after 5s and the goroutine dump, and no assertion text anywhere in quickfix.

In the dump, the test goroutine is in httptest.(*Server).Close, waiting in sync.WaitGroup.Wait, called from my deferred srv.Close(). The handler goroutine is in chan receive, and a third is in select inside persistConn.roundTrip.

-count=1, -race and DisableKeepAlives change nothing. The JSON test in the same file passes.

gate := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	<-gate // gateway holds the ack
	w.WriteHeader(http.StatusAccepted)
}))
defer srv.Close()

c := NewClient(srv.URL, log.New(io.Discard, "", 0))
errc := make(chan error, 1)
go func() { errc <- c.Send(context.Background(), testBatch()) }()

time.Sleep(100 * time.Millisecond)
if got := c.Pending(); got != 1 {
	t.Fatalf("Pending() = %d while ack is held, want 1", got)
}

close(gate)

Why would Close block here, and where did my failure message go?

The conversation continues 3

Follow the reasoning
  1. Permalink

    Close waits for outstanding requests, that's in the docs: https://pkg.go.dev/net/http/httptest#Server.Close

    your handler is parked on <-gate, so keep-alive isn't the problem. if the Fatalf fires, the defer runs Close before close(gate) ever happens. so it hangs. do defer close(gate) right after you make it, and run go test -v 2>&1 | pbcopy to grab the output.

  2. Permalink

    Ah, so the Fatalf does fire, and Close then waits on my own handler. Makes sense, I stared at the dump and didn't connect it. Thanks.

    One thing about order: defers run last-in first-out, so defer close(gate) placed after defer srv.Close() runs before it, good. But my test also calls close(gate) explicitly to release the ack and then reads errc. That would be a double close and a panic on the happy path. I think a sync.Once wrapped around the close, called in both places, is the cleanest way. Or is there a simpler pattern?

    Second question: is the missing message just buffering? With ./... and no -v I assume go test holds output until the package finishes, and a timeout panic kills it first. Is it possible that -v streams the Fatalf line before the hang? I'll try it. No pbcopy here, so I'll redirect to a file and open it in nvim ))

  3. Permalink

    Your Fatalf runs the deferred srv.Close() while the handler is still parked on <-gate. Close waits for that handler, so it's pretty much a deadlock. Release the gate first, before Close can run. The message is lost because the timeout panic kills the process before the buffered output is flushed. Try -v.

Other sides of the same question

  1. -WhatIf cleanup script says 13 expired archives but I only have 9 files total

    tidying old backup archives on my fedora laptop (pwsh 7.4, emacs, and yes this is becoming a demo for my class). the script f…

    Replies 2Latest activity powershell
  2. My init set fill-column for exactly one buffer

    Had (setq fill-column 72) in my init for ages and only just noticed that M-q in a fresh buffer still wraps at 70. (In *scratc…

    Replies 3Latest activity emacs-lisp
  3. SilentlyContinue is On Error Resume Next with better spelling

    Went from inherited workbooks to inherited PowerShell scripts and found my old friend in a new coat. -ErrorAction SilentlyCon…

    Replies 0Latest activity powershell