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
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"))))