fix(19.3-06): cors:<profile> headers on every response of the route (CR-01)

- Router.wrap applies the route's profile outside recovery, body limit and
  locale, whatever its position in the list or the group nesting
- bufferedResponse.commit merges Vary instead of replacing it
- TestCORSProfileHeadersOnRefusals, TestCORSProfileVaryKeptWithHandlerVary
  required by name in the gate
This commit is contained in:
Jakub Zych
2026-10-15 22:24:08 +02:00
parent e9ad640999
commit 072353663e
7 changed files with 226 additions and 11 deletions

View File

@@ -413,6 +413,13 @@ func (r *Router) wrap(rt route) (http.Handler, error) {
return nil, fmt.Errorf("surf: plugin %q: %w", rt.pluginID, err)
}
h = orgSlot(h)
// The route's cors:<profile> (on the route or an enclosing group) is
// applied below, outside recovery, the body limit and locale, so every
// answer of the route carries it, a guard's refusal included.
profile, err := routeCORSProfile(rt)
if err != nil {
return nil, err
}
for i := len(rt.middleware) - 1; i >= 0; i-- {
name := rt.middleware[i]
if named, ok := r.named[name]; ok {
@@ -437,6 +444,8 @@ func (r *Router) wrap(rt route) (http.Handler, error) {
if _, ok := r.corsProfiles[param]; !ok {
return nil, fmt.Errorf("surf: plugin %q route %s %s references unknown CORS profile %q (define http.cors_profiles.%s)", rt.pluginID, rt.method, rt.path, param, param)
}
// Never composed at its list position: see below.
continue
}
if base == "turnstile" && param != "open" && param != "closed" {
return nil, fmt.Errorf("surf: plugin %q route %s %s: turnstile:%s must be turnstile:open or turnstile:closed", rt.pluginID, rt.method, rt.path, param)
@@ -467,6 +476,12 @@ func (r *Router) wrap(rt route) (http.Handler, error) {
} else {
h = recoverJSON(h)
}
// The CORS profile wraps recovery, so a refusal from a guard listed
// before it, a 413 from the body limit and a recovered 500 all carry
// the profile's headers; otherwise the browser hides them from the page.
if profile != "" {
h = r.corsProfiles[profile].profileMiddleware(h)
}
// Outermost: name the matched route for the access log (accesslog.go).
h = recordPattern(rt.method+" "+rt.path, h)
return h, nil
@@ -520,7 +535,8 @@ func BuildRouter(app *backpack.App, plugins []party.Plugin) (*Router, error) {
}
r.corsProfiles = map[string]*corsPolicy{}
if err := r.RegisterMiddlewareFactory("surf", "cors", func(param string) pact.Middleware {
// wrap refuses an unknown profile before the factory runs.
// The name stays reserved for surf, but wrap applies the profile
// itself, outermost, and never calls this factory for it.
return r.corsProfiles[param].profileMiddleware
}); err != nil {
return nil, err
@@ -793,6 +809,18 @@ func (*bufferedResponse) Flush() {}
func (w *bufferedResponse) commit(dst http.ResponseWriter) {
for key, values := range w.header {
if key == "Vary" {
// Merged, not replaced: a Vary: Origin a CORS wrapper set on
// dst survives a handler that sets its own Vary.
for _, line := range values {
for _, v := range strings.Split(line, ",") {
if v = strings.TrimSpace(v); v != "" {
addVary(dst.Header(), v)
}
}
}
continue
}
dst.Header()[key] = append([]string(nil), values...)
}
status := w.status