Repository navigation
fix!: render layouts innermost so route middleware headers and cookies survive - #522
Merged
Merged
Conversation
…s survive LayoutMiddleware is global, so it wrapped every route middleware and replaced a successful controller result with a freshly rendered layout, dropping any Set-Cookie, cache or custom header that route middleware or the controller put on the response. Routing gains a #[RunsInnermost] attribute for global middleware: marked classes run after route middleware, directly around the controller, in declaration order, and #[WithoutMiddleware] still excludes them. LayoutMiddleware carries it, so route middleware now decorates the rendered layout response, and denials from route middleware answer before any layout renders. Headers and cookies the controller set on its own result are copied onto the layout response (the layout wins on conflicts; Content-Type/Content-Length describe the layout body). The #440 pass-through of non-2xx results is unchanged. Fixes #512 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyjR4cJTfro54iFZ9CWkv1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LayoutMiddlewareis global, so it wrapped every route middleware and replaced a successful controller result with the rendered layout, droppingSet-Cookie, cache and custom headers that route middleware or the controller set.marko/routinggains#[RunsInnermost]for global middleware: marked classes run after route middleware, directly around the controller (declaration order kept among them;#[WithoutMiddleware]still excludes them). Same shape as the existing#[RunsOnUnmatched].LayoutMiddlewareis marked#[RunsInnermost], so route middleware now decorates the rendered layout response, and route-middleware denials (auth redirect,#[Can]403) answer before any layout renders. Headers and cookies the controller put on its own result are copied onto the layout response (layout wins on conflicts;Content-Type/Content-Lengthalways describe the layout body).Locationresults is unchanged.Deviation from the suggested fix
The issue suggested a "result decorator" hook collected from the container. The container has no tagged-service collection, so that would need a new
module.phpkey plumbed through core's manifest parser, module cache and discovery cache. An attribute on the global middleware class gets the same innermost placement with a change confined toRouter::middlewareFor(). It also keepsLayoutMiddlewareinglobalMiddleware, so existing#[WithoutMiddleware(LayoutMiddleware::class)]exclusions keep working and no module entry disappears.Breaking
Route middleware on a
#[Layout]route now sees the rendered layout response from$next()instead of the raw controller response, andLayoutMiddlewarenow runs after route middleware instead of before it. Middleware that inspected the controller's raw body on a layout route will see the layout HTML instead. There is no global switch back to the old order. A route that needs it can exclude the layout with#[WithoutMiddleware(LayoutMiddleware::class)]and attach its own route middleware that callsLayoutProcessorInterface::process()at the position it wants.Test plan
RunsInnermostTest(routing): marked global middleware runs after route middleware and directly around the controller, declaration order is kept among marked middleware, and#[WithoutMiddleware]still excludes itLayoutMiddlewareRouterTest: a cookie,Cache-Controland a custom header set by route middleware are present on a layout-rendered 2xx; controller-set headers and cookies are carried over with the layout winning conflicts; the existing fix: pass inner denials through layout middleware #440 tests (302 auth redirect, thrown 403, controller 404) still pass with no layout renderLayoutMiddlewareTest:LayoutMiddlewarecarries#[RunsInnermost]; non-2xx pass-through dataset unchangedcomposer test,cs:check,phpstangreen locallyFixes #512
🤖 Generated with Claude Code
https://claude.ai/code/session_01KyjR4cJTfro54iFZ9CWkv1