Answer redirected writes with 303 so expired sessions show the login page, not a 405 - #1680
Merged
ignacionelson merged 1 commit intoAug 24, 2026
Conversation
A redirect born in exception handling - the guest redirect after an expired login, above all - never travels back through the middleware stack, so Inertia's usual 302-to-303 upgrade cannot reach it. Browsers follow a 302 by replaying the request method on the redirect target (only POST is downgraded to GET), so a widget save whose session just died replays as PUT /login and fails with a 405 that hides the real "please sign in again" (projectsend#1673). Repeat the upgrade in the exception pipeline: any 302 answered to a PUT, PATCH or DELETE becomes a 303. Reads keep their 302, POST needs nothing - browsers already downgrade it.
ignacionelson
added a commit
that referenced
this pull request
Aug 24, 2026
#1680 fixed the redirect rendered from an exception and said plainly what it did not cover: EnsureSetupIsComplete, EnsureAccountIsActive and EnforceTwoFactor answer before HandleInertiaRequests is ever entered, so a response they return never unwinds through Inertia's 302 to 303 upgrade either. Same 405, reached a different way — an account deactivated while its owner was part-way through a form, or one being made to enrol in two-factor. The rule now lives in one place rather than four. Three copies of "if the method is PUT, PATCH or DELETE" is how the fourth caller gets it wrong, and WriteSafeRedirect can carry the explanation of why 303 — which is worth more than the three lines it replaces, because nothing about a bare setStatusCode call says what a browser does with a 302. PUT /timezone is the route the setup test uses: it is one of only two writes a guest can reach and the only one that middleware does not exempt, so the case is real rather than defensive. All three new tests were run against the unfixed middleware and fail there. Extends the work of @denkfabrik-li, who found the gap and wrote it down.
This was referenced Aug 25, 2026
[14.x] Redirect unauthenticated PUT/PATCH/DELETE requests with 303 See Other
laravel/framework#61336
Closed
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.
Fixes #1673
What the reporter saw
Browsers follow a 302 by replaying the request method on the redirect target — only POST is downgraded to GET. So when a widget save (
PUT /dashboard/widgets) runs into an expired session and gets redirected to the login page, the browser replaysPUT /login, which only accepts GET/POST → 405, and Inertia shows the error modal instead of the login screen. ThePATCH … 405in the original report is the same failure on a PATCH-based settings save. It only strikes when a write request gets redirected, which is why it looked so random — the reporter's sessions were being killed constantly by the untrusted-proxy problem (#1672, addressed by #1674).Why Inertia's own 303 upgrade didn't cover this
Inertia\Middlewareupgrades 302→303 for PUT/PATCH/DELETE, but a redirect created during exception handling — the guest redirect afterAuthenticationException, above all — never travels back through the middleware stack, so the upgrade never runs. Reproduced directly: an unauthenticatedPUT /dashboard/widgetswith a valid CSRF token answers302 Location: /login.The fix
A
respondhook inbootstrap/app.php: any 302 rendered from an exception in reply to a PUT/PATCH/DELETE becomes a 303 ("see other" — follow with GET). Reads keep their 302; POST needs nothing.Tests
PUT(widget save) → 303 to the login page (was 302, failing before the fix)DELETE→ 303 to the login pageGET→ still a plain 302Not covered on purpose
The direct (non-exception) redirects issued by
EnsureSetupIsComplete,EnsureAccountIsActiveandEnforceTwoFactorsit outsideHandleInertiaRequestsin the web group, so a write request caught by one of those can still produce the same 405 pattern (e.g. an account deactivated mid-session saving a form). Much rarer, and each wants its own decision about the right status — happy to follow up if wanted.