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
This commit is contained in:
Jakub Zych
2026-09-20 21:05:44 +02:00
parent f6ba67a693
commit 93d63c351b

View File

@@ -1,6 +1,7 @@
package surf package surf
import ( import (
"bytes"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@@ -10,6 +11,7 @@ import (
"git.golem15.com/golem15/summercms/pact" "git.golem15.com/golem15/summercms/pact"
"git.golem15.com/golem15/summercms/party" "git.golem15.com/golem15/summercms/party"
"git.golem15.com/golem15/summercms/towel" "git.golem15.com/golem15/summercms/towel"
"git.golem15.com/golem15/summercms/wire"
) )
// Use names a middleware list for Group, matching PHP ->middleware(). // 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 { func recoverJSON(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buffered := newBufferedResponse()
defer func() { defer func() {
if rec := recover(); rec != nil { if rec := recover(); rec != nil {
w.Header().Set("Content-Type", "application/json") wire.WriteOpaque500(w)
w.WriteHeader(http.StatusInternalServerError) return
_, _ = w.Write([]byte(`{"error":true,"message":"Internal server error"}`))
} }
buffered.commit(w)
}() }()
next.ServeHTTP(w, r) next.ServeHTTP(buffered, r)
}) })
} }
func recoverBare(next http.Handler) http.Handler { func recoverBare(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buffered := newBufferedResponse()
defer func() { defer func() {
if rec := recover(); rec != nil { if rec := recover(); rec != nil {
w.WriteHeader(http.StatusInternalServerError) 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 { func locale(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r.WithContext(towel.WithLocale(r.Context(), r.Header.Get("Accept-Language")))) next.ServeHTTP(w, r.WithContext(towel.WithLocale(r.Context(), r.Header.Get("Accept-Language"))))