Skip to content

fix(app-router): redirect() returns 307 on document load#1571

Open
james-elicx wants to merge 1 commit into
mainfrom
fix/redirect-307-document-load
Open

fix(app-router): redirect() returns 307 on document load#1571
james-elicx wants to merge 1 commit into
mainfrom
fix/redirect-307-document-load

Conversation

@james-elicx
Copy link
Copy Markdown
Member

Closes #1530

Summary

  • Document (non-RSC) navigation to a page calling redirect() now returns HTTP 307 with a Location header.
  • Root cause: the App Router prerender harness forwarded requests through fetch with its default redirect: "follow" behavior, so the harness silently followed the 307 server-side and persisted the destination page's HTML under the redirecting route's filename. The seeded cache then served that body with status 200 for every document request.
  • Fix: set redirect: "manual" on the App Router prerender rscHandler, matching what the pages-prerender helper already does. The redirect now propagates back, the route is marked skipped during prerender, and at runtime the live render path emits the proper 307 + Location via buildAppPageSpecialErrorResponse.
  • RSC flight-payload handling (RSC redirect returns raw 307 instead of 200 with flight redirect payload #1347) is unchanged — redirect-test.rsc still returns 200 with NEXT_REDIRECT in the flight body; metadata-redirect-test still returns 200 on document load.

Test plan

  • New regression test in tests/prerender.test.ts asserts /redirect-test is skipped and no HTML/RSC are written under the redirecting filename
  • New end-to-end test in tests/app-router.test.ts asserts the production server returns 307 + Location for a document load
  • Existing /redirect-test.rsc and /metadata-redirect-test tests still pass (RSC + metadata-redirect path untouched)
  • Manually verified with vinext build --prerender-all + vinext start: document fetch of /redirect-test returns 307, .rsc fetch returns 200 with flight payload
  • vp check passes

The App Router prerender harness forwarded requests through `fetch` with
fetch's default `redirect: "follow"` behavior. When a page called
`redirect()` the prod server emitted a 307, fetch silently followed the
Location header to the destination page, and the harness wrote the
destination's HTML under the redirecting route's filename. The seeded
cache then served that body with status 200 for every document request,
breaking Next.js parity (test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts
expects 307).

Set `redirect: "manual"` on the App Router prerender `rscHandler` so the
original 3xx surfaces back. `htmlRender.ok` is then false, the route is
marked skipped, and at runtime the document request hits the live
render path that already builds a proper 307 + Location response via
`buildAppPageSpecialErrorResponse`. RSC flight-payload handling (#1347)
is untouched.

The pages-prerender `renderPage` helper already used `redirect: "manual"`,
so this brings the App Router phase in line.

Closes #1530
@james-elicx
Copy link
Copy Markdown
Member Author

/bigbonk review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 22, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1571

commit: e275f10

@github-actions
Copy link
Copy Markdown

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
web preview production

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix. The root cause analysis is correct — fetch's default redirect: "follow" was silently chasing the 307 inside the prerender harness, so the destination page's HTML got persisted under the redirecting route's filename. At runtime the seeded cache then served that body as a 200.

Code changeredirect: "manual" on the App Router rscHandler fetch (line 914) is the right one-liner. The 307 now propagates back as a non-ok response, the existing !response.ok check at line 1187 catches it, htmlRender.ok is false, and the speculative route falls through to "skipped". At runtime the live render path builds the proper 307 + Location via buildAppPageSpecialErrorResponse. Matches the pages-prerender renderPage helper at line 561 that already had redirect: "manual".

Tests are solid:

  • Prerender test asserts the route is "skipped" and neither .html nor .rsc files are written for the redirecting filename — this is the key invariant that was violated.
  • Prod server test asserts 307 + Location: /about on a document load, which is the user-facing symptom from #1530.
  • Both tests reference the ported Next.js test (test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts), per AGENTS.md guidance.

No concerns. The comment block explaining why redirect: "manual" is needed is appreciated — future readers will understand the interaction between fetch semantics and the prerender cache.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 22, 2026

Approved PR #1571. The fix is correct and minimal — adding redirect: "manual" to the App Router prerender rscHandler fetch prevents fetch from silently following the 307 redirect server-side, which was causing the destination page's HTML to be cached under the redirecting route's filename. The tests cover both the prerender invariant (route is skipped, no files written) and the runtime behavior (307 + Location on document load).

github run

@james-elicx james-elicx marked this pull request as ready for review May 22, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App Router: document (non-RSC) navigation to a redirect()ing page returns 200 instead of 307

1 participant