From d12e887858ab32b9fd627febb1586a36afdecabe Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 7 Apr 2026 07:46:06 +0800 Subject: [PATCH 1/2] fix: keep RunShutdown on HTTP path --- serve.go | 32 ++++++++++++---------- serve_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 serve_test.go diff --git a/serve.go b/serve.go index f3ddc5f..1825b32 100644 --- a/serve.go +++ b/serve.go @@ -46,26 +46,30 @@ func getShutdownTimeout(timeouts []time.Duration) time.Duration { return defaultShutdownTimeout } +// serveServer 根据显式指定的启动模式运行 HTTP 或 HTTPS 服务器. +func serveServer(srv *http.Server, serveTLS bool) error { + if serveTLS { + // 对于 HTTPS 服务器,如果 srv.TLSConfig.Certificates 已配置, + // ListenAndServeTLS 的前两个参数可以为空字符串 + return srv.ListenAndServeTLS("", "") + } + + return srv.ListenAndServe() +} + // runServer 是一个内部辅助函数,负责在一个新的 goroutine 中启动一个 http.Server, // 并处理其启动失败的致命错误 // serverType 用于在日志中标识服务器类型 (例如 "HTTP", "HTTPS") -func runServer(serverType string, srv *http.Server) { +func runServer(serverType string, srv *http.Server, serveTLS bool) { go func() { - var err error protocol := "http" - if srv.TLSConfig != nil { + if serveTLS { protocol = "https" } log.Printf("Touka %s server listening on %s://%s", serverType, protocol, srv.Addr) - if srv.TLSConfig != nil { - // 对于 HTTPS 服务器,如果 srv.TLSConfig.Certificates 已配置, - // ListenAndServeTLS 的前两个参数可以为空字符串 - err = srv.ListenAndServeTLS("", "") - } else { - err = srv.ListenAndServe() - } + err := serveServer(srv, serveTLS) // 如果服务器停止不是因为被优雅关闭 (http.ErrServerClosed), // 则认为是一个严重错误,并终止程序 @@ -236,7 +240,7 @@ func (engine *Engine) RunShutdown(addr string, timeouts ...time.Duration) error engine.ServerConfigurator(srv) } - runServer("HTTP", srv) + runServer("HTTP", srv, false) return handleGracefulShutdown([]*http.Server{srv}, getShutdownTimeout(timeouts), engine.LogReco) } @@ -293,7 +297,7 @@ func (engine *Engine) RunTLS(addr string, tlsConfig *tls.Config, timeouts ...tim engine.ServerConfigurator(srv) } - runServer("HTTPS", srv) + runServer("HTTPS", srv, true) return handleGracefulShutdown([]*http.Server{srv}, getShutdownTimeout(timeouts), engine.LogReco) } @@ -361,8 +365,8 @@ func (engine *Engine) RunTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Con } // --- 启动服务器和优雅关闭 --- - runServer("HTTPS", httpsSrv) - runServer("HTTP Redirect", httpSrv) + runServer("HTTPS", httpsSrv, true) + runServer("HTTP Redirect", httpSrv, false) return handleGracefulShutdown([]*http.Server{httpsSrv, httpSrv}, getShutdownTimeout(timeouts), engine.LogReco) } diff --git a/serve_test.go b/serve_test.go new file mode 100644 index 0000000..01d639f --- /dev/null +++ b/serve_test.go @@ -0,0 +1,76 @@ +package touka + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net" + "net/http" + "testing" + "time" +) + +func TestServeServerHTTPModeIgnoresTLSConfig(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen on ephemeral port: %v", err) + } + addr := listener.Addr().String() + if err := listener.Close(); err != nil { + t.Fatalf("close temporary listener: %v", err) + } + + srv := &http.Server{ + Addr: addr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) + }), + // RunShutdown uses the HTTP startup path and must not let a shared + // ServerConfigurator accidentally turn it into HTTPS. + TLSConfig: &tls.Config{}, + } + + errCh := make(chan error, 1) + go func() { + errCh <- serveServer(srv, false) + }() + + client := &http.Client{Timeout: 200 * time.Millisecond} + var resp *http.Response + requestURL := "http://" + addr + + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + resp, err = client.Get(requestURL) + if err == nil { + break + } + time.Sleep(20 * time.Millisecond) + } + if err != nil { + t.Fatalf("expected HTTP server to accept plain HTTP with TLSConfig set: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("read response body: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status code: got %d want %d", resp.StatusCode, http.StatusOK) + } + if string(body) != "ok" { + t.Fatalf("unexpected body: got %q want %q", string(body), "ok") + } + + shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if err := srv.Shutdown(shutdownCtx); err != nil { + t.Fatalf("shutdown server: %v", err) + } + + if err := <-errCh; !errors.Is(err, http.ErrServerClosed) { + t.Fatalf("serveServer should stop with ErrServerClosed after shutdown, got %v", err) + } +} From 7db3d32d7b9f807864ff6a4692b13f949db8a316 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 7 Apr 2026 07:51:39 +0800 Subject: [PATCH 2/2] test: improve serve startup failure diagnostics --- serve_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/serve_test.go b/serve_test.go index 01d639f..6092f7b 100644 --- a/serve_test.go +++ b/serve_test.go @@ -49,7 +49,12 @@ func TestServeServerHTTPModeIgnoresTLSConfig(t *testing.T) { time.Sleep(20 * time.Millisecond) } if err != nil { - t.Fatalf("expected HTTP server to accept plain HTTP with TLSConfig set: %v", err) + select { + case serveErr := <-errCh: + t.Fatalf("expected HTTP server to accept plain HTTP with TLSConfig set: request error=%v, serve error=%v", err, serveErr) + default: + t.Fatalf("expected HTTP server to accept plain HTTP with TLSConfig set: %v", err) + } } defer resp.Body.Close()