From cd2da57ba956fc81176818f2a70c045bc889db1d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 16:50:39 +0000 Subject: [PATCH] fix: Logger always records status 0 due to two compounding bugs Bug 1 (mux.go): Kit.Plug bridge called next.ServeHTTP(w, r) with the captured outer writer, ignoring any ctx.Response swap made by the plug (e.g. Logger replacing the writer with a ResponseRecorder). Fix: use c.Response and c.Request so the downstream chi handler receives the swapped writer. Bug 2 (recorder.go): ResponseRecorder.Write did not default Status to 200 when called without a prior WriteHeader, unlike net/http. Fix: set Status = 200 on the first Write when Status is still 0. Adds regression tests for both bugs. https://claude.ai/code/session_01SYHXLF9NZsd9AvunJ6az8v --- pkg/util/httputil/recorder.go | 4 ++++ pkg/util/httputil/recorder_test.go | 16 ++++++++++++++++ pkg/webkit/mux.go | 3 +-- pkg/webkit/mux_test.go | 24 ++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/pkg/util/httputil/recorder.go b/pkg/util/httputil/recorder.go index 758e6e99..551c08e7 100644 --- a/pkg/util/httputil/recorder.go +++ b/pkg/util/httputil/recorder.go @@ -23,7 +23,11 @@ func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder { } // Write writes the response body to the captured buffer and the underlying ResponseWriter. +// Mirrors net/http behaviour: if WriteHeader has not been called, status defaults to 200. func (r *ResponseRecorder) Write(b []byte) (int, error) { + if r.Status == 0 { + r.Status = http.StatusOK + } r.Body.Write(b) return r.ResponseWriter.Write(b) } diff --git a/pkg/util/httputil/recorder_test.go b/pkg/util/httputil/recorder_test.go index 7258d97d..9725e278 100644 --- a/pkg/util/httputil/recorder_test.go +++ b/pkg/util/httputil/recorder_test.go @@ -19,6 +19,22 @@ func TestResponseRecorder_Write(t *testing.T) { assert.Equal(t, testBody, rr.Body.Bytes()) } +func TestResponseRecorder_Write_DefaultsStatusTo200(t *testing.T) { + // Write without a prior WriteHeader must record 200, mirroring net/http behaviour. + rr := NewResponseRecorder(httptest.NewRecorder()) + _, err := rr.Write([]byte("hello")) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rr.Status) +} + +func TestResponseRecorder_Write_PreservesExplicitStatus(t *testing.T) { + rr := NewResponseRecorder(httptest.NewRecorder()) + rr.WriteHeader(http.StatusCreated) + _, err := rr.Write([]byte("created")) + require.NoError(t, err) + assert.Equal(t, http.StatusCreated, rr.Status) +} + func TestResponseRecorder_WriteHeader(t *testing.T) { rr := NewResponseRecorder(httptest.NewRecorder()) rr.WriteHeader(http.StatusOK) diff --git a/pkg/webkit/mux.go b/pkg/webkit/mux.go index 9ad94c5f..7dc500e7 100644 --- a/pkg/webkit/mux.go +++ b/pkg/webkit/mux.go @@ -71,8 +71,7 @@ func (k *Kit) Plug(plugs ...Plug) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := NewContext(w, r) h := plug(func(c *Context) error { - r = c.Request - next.ServeHTTP(w, r) + next.ServeHTTP(c.Response, c.Request) return nil }) if err := h(ctx); err != nil { diff --git a/pkg/webkit/mux_test.go b/pkg/webkit/mux_test.go index d57c0d4f..f167cd58 100644 --- a/pkg/webkit/mux_test.go +++ b/pkg/webkit/mux_test.go @@ -65,6 +65,30 @@ func TestKit_Plug(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) }) + t.Run("Response writer swap in plug is visible to downstream handler", func(t *testing.T) { + // Regression: Plug used to call next.ServeHTTP(w, r) with the captured outer + // writer, discarding any ctx.Response swap made by the plug (e.g. a recorder). + t.Parallel() + app := New() + + var recorded int + app.Plug(func(next Handler) Handler { + return func(ctx *Context) error { + rr := httptest.NewRecorder() + ctx.Response = rr + err := next(ctx) + recorded = rr.Code + return err + } + }) + app.Get("/", func(ctx *Context) error { + return ctx.String(http.StatusCreated, "hi") + }) + + app.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)) + assert.Equal(t, http.StatusCreated, recorded, "plug's recorder must see the status written by the handler") + }) + t.Run("Runs on OPTIONS preflight to GET-only route", func(t *testing.T) { t.Parallel() app := New()