From 93d63c351ba063955d348f6aa3388482b8bea87c Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 20 Sep 2026 21:05:44 +0200 Subject: [PATCH] fix(06-09): buffer route responses before recovery - Discard partial route output when house or raw handlers panic - Commit private headers status and body only after successful return --- surf/router.go | 57 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/surf/router.go b/surf/router.go index a35ade5..bf734e5 100644 --- a/surf/router.go +++ b/surf/router.go @@ -1,6 +1,7 @@ package surf import ( + "bytes" "fmt" "net/http" "strings" @@ -10,6 +11,7 @@ import ( "git.golem15.com/golem15/summercms/pact" "git.golem15.com/golem15/summercms/party" "git.golem15.com/golem15/summercms/towel" + "git.golem15.com/golem15/summercms/wire" ) // Use names a middleware list for Group, matching PHP ->middleware(). @@ -519,28 +521,73 @@ func corsOrigins(app *backpack.App) []string { func recoverJSON(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buffered := newBufferedResponse() defer func() { if rec := recover(); rec != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(`{"error":true,"message":"Internal server error"}`)) + wire.WriteOpaque500(w) + return } + buffered.commit(w) }() - next.ServeHTTP(w, r) + next.ServeHTTP(buffered, r) }) } func recoverBare(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buffered := newBufferedResponse() defer func() { if rec := recover(); rec != nil { w.WriteHeader(http.StatusInternalServerError) + return } + buffered.commit(w) }() - next.ServeHTTP(w, r) + next.ServeHTTP(buffered, r) }) } +type bufferedResponse struct { + header http.Header + status int + body bytes.Buffer +} + +func newBufferedResponse() *bufferedResponse { + return &bufferedResponse{header: make(http.Header)} +} + +func (w *bufferedResponse) Header() http.Header { return w.header } + +func (w *bufferedResponse) WriteHeader(status int) { + if w.status == 0 { + w.status = status + } +} + +func (w *bufferedResponse) Write(p []byte) (int, error) { + if w.status == 0 { + w.status = http.StatusOK + } + return w.body.Write(p) +} + +// Flush deliberately does not expose or commit the destination writer. Route +// output becomes visible only after the handler returns successfully. +func (*bufferedResponse) Flush() {} + +func (w *bufferedResponse) commit(dst http.ResponseWriter) { + for key, values := range w.header { + dst.Header()[key] = append([]string(nil), values...) + } + status := w.status + if status == 0 { + status = http.StatusOK + } + dst.WriteHeader(status) + _, _ = dst.Write(w.body.Bytes()) +} + func locale(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r.WithContext(towel.WithLocale(r.Context(), r.Header.Get("Accept-Language"))))