From 7b84189e0cf74daec952879da6490e3cb1b2bee3 Mon Sep 17 00:00:00 2001 From: c879873067877881111 Date: Mon, 10 Aug 2026 10:41:00 +0800 Subject: [PATCH 1/2] fix: restore original writer so unmatched routes still return 404 When the middleware is registered globally via Use(), gin runs it inside serveError(), which sets the status on c.writermem and then writes the default body through c.Writer after the middleware returns. The finish branch freed the buffer but left c.Writer pointing at the timeout Writer, so that write landed in a nil body and was silently dropped. The status was never flushed either, so net/http sent an implicit 200 with an empty body while gin's logger reported 404. Restore c.Writer to the original writer once the buffer is flushed, the same way the panic branch already does. This also fixes the 405 response for HandleMethodNotAllowed, which goes through the same path. --- timeout.go | 5 +++++ timeout_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/timeout.go b/timeout.go index 6cdfc10..bc72258 100644 --- a/timeout.go +++ b/timeout.go @@ -122,6 +122,11 @@ func New(opts ...Option) gin.HandlerFunc { } tw.FreeBuffer() bufPool.Put(buffer) + // Restore the original writer so anything gin writes after this + // middleware returns - such as the default 404 body from + // serveError() when no route matched - reaches the real + // ResponseWriter instead of the freed buffer. + c.Writer = w case <-timer.C: tw.mu.Lock() diff --git a/timeout_test.go b/timeout_test.go index c6f8cfc..3aa7546 100644 --- a/timeout_test.go +++ b/timeout_test.go @@ -296,3 +296,38 @@ func TestContextDeadlineSet(t *testing.T) { assert.True(t, hasDeadline, "request context should have a deadline set by the middleware") assert.Equal(t, http.StatusOK, w.Code) } + +func TestNoRouteWithUse(t *testing.T) { + r := gin.New() + r.Use(New( + WithTimeout(1 * time.Second), + )) + r.GET("/hello", func(c *gin.Context) { + c.String(http.StatusOK, "world") + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), "GET", "/no/such/route", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Equal(t, "404 page not found", w.Body.String()) +} + +func TestMethodNotAllowedWithUse(t *testing.T) { + r := gin.New() + r.HandleMethodNotAllowed = true + r.Use(New( + WithTimeout(1 * time.Second), + )) + r.GET("/hello", func(c *gin.Context) { + c.String(http.StatusOK, "world") + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), "POST", "/hello", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusMethodNotAllowed, w.Code) + assert.Equal(t, "405 method not allowed", w.Body.String()) +} From d09a53c3bf6b8fd8edc613d80d5baae42fb84ec4 Mon Sep 17 00:00:00 2001 From: c879873067877881111 Date: Tue, 11 Aug 2026 19:34:16 +0800 Subject: [PATCH 2/2] fix: restore the writer on the timeout path too FreeBuffer() sets the timeout Writer's size to -1, and the timer branch left c.Writer pointing at it, so middleware that inspects c.Writer after c.Next() read -1 instead of the size of the response actually written. gin's own logger reports that as BodySize. Restore the writer next to c.Abort(), after the wait on the handler goroutine, since that is the point where touching c is safe. Reported by @zHElEARN. --- timeout.go | 7 ++++++- timeout_test.go | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/timeout.go b/timeout.go index bc72258..383c2fe 100644 --- a/timeout.go +++ b/timeout.go @@ -150,7 +150,12 @@ func New(opts ...Option) gin.HandlerFunc { case <-panicChan: } - // Goroutine is done. Safe to modify c.index now. + // Goroutine is done. Safe to modify c and c.index now. Restoring the + // writer matters here too: FreeBuffer left tw reporting Size() == -1, + // so middleware that inspects c.Writer after c.Next() - gin's own + // logger reading BodySize, for one - would read that instead of the + // timeout response actually written to w. + c.Writer = w c.Abort() } } diff --git a/timeout_test.go b/timeout_test.go index 3aa7546..aec379a 100644 --- a/timeout_test.go +++ b/timeout_test.go @@ -331,3 +331,26 @@ func TestMethodNotAllowedWithUse(t *testing.T) { assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, "405 method not allowed", w.Body.String()) } + +func TestOuterMiddlewareSeesResponseSize(t *testing.T) { + var size int + r := gin.New() + // Runs before the timeout middleware, so after c.Next() it reads whatever + // writer was left on the context - the same position as gin.Logger(). + r.Use(func(c *gin.Context) { + c.Next() + size = c.Writer.Size() + }) + r.Use(New( + WithTimeout(5 * time.Millisecond), + )) + r.GET("/", emptySuccessResponse) + + w := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusRequestTimeout, w.Code) + assert.Equal(t, len(http.StatusText(http.StatusRequestTimeout)), size, + "outer middleware should see the size of the timeout response, not the freed buffer") +}