From a4f8015caa51d61c654e228a395759bd7a403205 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 21 Jan 2026 10:57:27 +0800 Subject: [PATCH] test(logging): add unit tests for `GinLogrusRecovery` middleware panic handling --- internal/logging/gin_logger.go | 6 +++ internal/logging/gin_logger_test.go | 60 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 internal/logging/gin_logger_test.go diff --git a/internal/logging/gin_logger.go b/internal/logging/gin_logger.go index 2dfbcfc2..b94d7afe 100644 --- a/internal/logging/gin_logger.go +++ b/internal/logging/gin_logger.go @@ -4,6 +4,7 @@ package logging import ( + "errors" "fmt" "net/http" "runtime/debug" @@ -112,6 +113,11 @@ func isAIAPIPath(path string) bool { // - gin.HandlerFunc: A middleware handler for panic recovery func GinLogrusRecovery() gin.HandlerFunc { return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { + if err, ok := recovered.(error); ok && errors.Is(err, http.ErrAbortHandler) { + // Let net/http handle ErrAbortHandler so the connection is aborted without noisy stack logs. + panic(http.ErrAbortHandler) + } + log.WithFields(log.Fields{ "panic": recovered, "stack": string(debug.Stack()), diff --git a/internal/logging/gin_logger_test.go b/internal/logging/gin_logger_test.go new file mode 100644 index 00000000..7de18338 --- /dev/null +++ b/internal/logging/gin_logger_test.go @@ -0,0 +1,60 @@ +package logging + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestGinLogrusRecoveryRepanicsErrAbortHandler(t *testing.T) { + gin.SetMode(gin.TestMode) + + engine := gin.New() + engine.Use(GinLogrusRecovery()) + engine.GET("/abort", func(c *gin.Context) { + panic(http.ErrAbortHandler) + }) + + req := httptest.NewRequest(http.MethodGet, "/abort", nil) + recorder := httptest.NewRecorder() + + defer func() { + recovered := recover() + if recovered == nil { + t.Fatalf("expected panic, got nil") + } + err, ok := recovered.(error) + if !ok { + t.Fatalf("expected error panic, got %T", recovered) + } + if !errors.Is(err, http.ErrAbortHandler) { + t.Fatalf("expected ErrAbortHandler, got %v", err) + } + if err != http.ErrAbortHandler { + t.Fatalf("expected exact ErrAbortHandler sentinel, got %v", err) + } + }() + + engine.ServeHTTP(recorder, req) +} + +func TestGinLogrusRecoveryHandlesRegularPanic(t *testing.T) { + gin.SetMode(gin.TestMode) + + engine := gin.New() + engine.Use(GinLogrusRecovery()) + engine.GET("/panic", func(c *gin.Context) { + panic("boom") + }) + + req := httptest.NewRequest(http.MethodGet, "/panic", nil) + recorder := httptest.NewRecorder() + + engine.ServeHTTP(recorder, req) + if recorder.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", recorder.Code) + } +}