From 7bac0e17ab32dd9faaa6c2f800633843c56e0ae4 Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Thu, 8 Oct 2026 14:44:12 +0200 Subject: [PATCH 1/3] fix: use without cancel for base server context --- http/server/server.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/http/server/server.go b/http/server/server.go index 93c1e4c..d62c347 100644 --- a/http/server/server.go +++ b/http/server/server.go @@ -149,7 +149,10 @@ func (s *GenericServer[T]) runServer( } srv := &http.Server{ - Addr: addr, + Addr: addr, + BaseContext: func(_ net.Listener) context.Context { + return context.WithoutCancel(ctx) + }, Handler: h, TLSConfig: tlsCfg, ReadHeaderTimeout: withDefault(s.ReadHeaderTimeout, time.Second), From 4c9896b35b56963aa6d8ee1c0ba0be665135c8d7 Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Thu, 8 Oct 2026 14:48:49 +0200 Subject: [PATCH 2/3] test: add test --- http/server/server_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/http/server/server_test.go b/http/server/server_test.go index e090542..4a5b08e 100644 --- a/http/server/server_test.go +++ b/http/server/server_test.go @@ -309,6 +309,88 @@ func TestGenericServer_RunShutdownCausesReadyzToFail(t *testing.T) { } } +func TestGenericServer_RunDoesNotImmediatelyCancelHandler(t *testing.T) { + stats := statter.New(statter.DiscardReporter, 10*time.Second) + log := logger.New(io.Discard, logger.LogfmtFormat(), logger.Error) + + lnCh := make(chan net.Listener, 1) + setTestHookServerServe(func(ln net.Listener) { + lnCh <- ln + }) + t.Cleanup(func() { setTestHookServerServe(nil) }) + + startedCh := make(chan struct{}) + proceedCh := make(chan struct{}) + ctxErrCh := make(chan error, 1) + h := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + close(startedCh) + <-proceedCh + ctxErrCh <- r.Context().Err() + }) + + srv := &GenericServer[context.Context]{ + Addr: "localhost:0", + Handler: h, + Stats: stats, + Log: log, + } + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + shutdownCh := make(chan struct{}) + go func() { + defer close(shutdownCh) + + err := srv.Run(ctx) + + assert.NoError(t, err) + }() + + var ln net.Listener + select { + case <-time.After(30 * time.Second): + require.Fail(t, "Timed out waiting for server listener") + case ln = <-lnCh: + } + + respCh := make(chan int, 1) + go func() { + statusCode, _ := requireDoRequest(t, "http://"+ln.Addr().String()+"/") + respCh <- statusCode + }() + + select { + case <-time.After(30 * time.Second): + require.Fail(t, "Timed out waiting for handler to start") + case <-startedCh: + } + + cancel() + time.Sleep(50 * time.Millisecond) + close(proceedCh) + + select { + case <-time.After(30 * time.Second): + require.Fail(t, "Timed out waiting for handler") + case err := <-ctxErrCh: + assert.NoError(t, err) + } + + select { + case <-time.After(30 * time.Second): + require.Fail(t, "Timed out waiting for response") + case statusCode := <-respCh: + assert.Equal(t, http.StatusOK, statusCode) + } + + select { + case <-time.After(30 * time.Second): + require.Fail(t, "Timed out waiting for server to shutdown") + case <-shutdownCh: + } +} + func TestGenericServer_RunHandlesServerError(t *testing.T) { stats := statter.New(statter.DiscardReporter, 10*time.Second) log := logger.New(io.Discard, logger.LogfmtFormat(), logger.Error) From a9ef986d0dc9e43e0ab09b1a1e9b1550bff57667 Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Thu, 8 Oct 2026 14:53:15 +0200 Subject: [PATCH 3/3] fix: linter issues --- http/server/server_test.go | 46 ++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/http/server/server_test.go b/http/server/server_test.go index 4a5b08e..c777db2 100644 --- a/http/server/server_test.go +++ b/http/server/server_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "errors" "io" "net" "net/http" @@ -354,10 +355,14 @@ func TestGenericServer_RunDoesNotImmediatelyCancelHandler(t *testing.T) { case ln = <-lnCh: } - respCh := make(chan int, 1) + type result struct { + statusCode int + err error + } + respCh := make(chan result, 1) go func() { - statusCode, _ := requireDoRequest(t, "http://"+ln.Addr().String()+"/") - respCh <- statusCode + statusCode, _, err := doRequest(t, "http://"+ln.Addr().String()+"/") + respCh <- result{statusCode: statusCode, err: err} }() select { @@ -374,14 +379,15 @@ func TestGenericServer_RunDoesNotImmediatelyCancelHandler(t *testing.T) { case <-time.After(30 * time.Second): require.Fail(t, "Timed out waiting for handler") case err := <-ctxErrCh: - assert.NoError(t, err) + require.NoError(t, err) } select { case <-time.After(30 * time.Second): require.Fail(t, "Timed out waiting for response") - case statusCode := <-respCh: - assert.Equal(t, http.StatusOK, statusCode) + case res := <-respCh: + require.NoError(t, res.err) + assert.Equal(t, http.StatusOK, res.statusCode) } select { @@ -469,9 +475,19 @@ func TestGenericServer_RunHandlesUnexpectedListenerClose(t *testing.T) { func requireDoRequest(t *testing.T, path string) (int, string) { t.Helper() + statusCode, body, err := doRequest(t, path) + require.NoError(t, err) + + return statusCode, body +} + +func doRequest(t *testing.T, path string) (int, string, error) { + t.Helper() + certPool := x509.NewCertPool() - ok := certPool.AppendCertsFromPEM(localhostCert) - require.True(t, ok, "failed to append cert to pool") + if ok := certPool.AppendCertsFromPEM(localhostCert); !ok { + return 0, "", errors.New("failed to append cert to pool") + } protos := &http.Protocols{} protos.SetHTTP2(true) @@ -490,19 +506,25 @@ func requireDoRequest(t *testing.T, path string) (int, string) { t.Cleanup(cancel) req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil) - require.NoError(t, err) + if err != nil { + return 0, "", err + } resp, err := client.Do(req) - require.NoError(t, err) + if err != nil { + return 0, "", err + } defer func() { _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() }() b, err := io.ReadAll(resp.Body) - require.NoError(t, err) + if err != nil { + return 0, "", err + } - return resp.StatusCode, string(b) + return resp.StatusCode, string(b), nil } func setTestHookServerServe(fn func(net.Listener)) {