[PR #779] [MERGED] fix: default redirect() to "push" in Server Action context #839

Closed
opened 2026-05-06 13:10:24 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/779
Author: @NathanDrake2406
Created: 4/4/2026
Status: Merged
Merged: 4/4/2026
Merged by: @james-elicx

Base: mainHead: fix/server-action-redirect-type


📝 Commits (1)

  • df4b44a fix: default redirect() to "push" in Server Action context

📊 Changes

5 files changed (+140 additions, -18 deletions)

View changed files

📝 packages/vinext/src/entries/app-rsc-entry.ts (+5 -2)
📝 packages/vinext/src/shims/navigation.ts (+21 -3)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+30 -12)
📝 tests/app-router.test.ts (+18 -0)
📝 tests/shims.test.ts (+66 -1)

📄 Description

Summary

  • redirect() called inside a Server Action always used "replace" navigation, breaking the Back button after form submissions. Next.js defaults to "push" in action context (via actionAsyncStorage.getStore()?.isAction).
  • Instead of adding a new AsyncLocalStorage, use an empty string sentinel in the digest when no explicit type is provided. The action handler resolves empty to "push", while SSR render contexts only extract location/statusCode (no type needed at HTTP level).
  • Also adds an optional type parameter to permanentRedirect() matching the Next.js signature (defaults to "replace").

How it works

  1. redirect("/foo") now produces digest NEXT_REDIRECT;;%2Ffoo (empty type field)
  2. redirect("/foo", "push") produces NEXT_REDIRECT;push;%2Ffoo (explicit, always respected)
  3. The action handler in app-rsc-entry.ts resolves empty to "push" (parts[1] || "push")
  4. The browser entry reads x-action-redirect-type header and uses window.location.assign() for push / window.location.replace() for replace

Next.js reference

Test plan

  • redirect() without explicit type produces empty sentinel in digest
  • redirect() with explicit "push" or "replace" preserves type in digest
  • Semicolon-in-URL encoding still prevents digest injection (updated assertion)
  • permanentRedirect() accepts optional type parameter
  • permanentRedirect() defaults to "replace" when no type given
  • Generated action handler defaults empty type to "push" (template assertion)
  • Entry template snapshots updated
  • All existing redirect/error-boundary/route-handler tests pass

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/779 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 4/4/2026 **Status:** ✅ Merged **Merged:** 4/4/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `fix/server-action-redirect-type` --- ### 📝 Commits (1) - [`df4b44a`](https://github.com/cloudflare/vinext/commit/df4b44ad8110ca7ab108346ebe2cace5fcca2cfb) fix: default redirect() to "push" in Server Action context ### 📊 Changes **5 files changed** (+140 additions, -18 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+5 -2) 📝 `packages/vinext/src/shims/navigation.ts` (+21 -3) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+30 -12) 📝 `tests/app-router.test.ts` (+18 -0) 📝 `tests/shims.test.ts` (+66 -1) </details> ### 📄 Description ## Summary - `redirect()` called inside a Server Action always used `"replace"` navigation, breaking the Back button after form submissions. Next.js defaults to `"push"` in action context (via `actionAsyncStorage.getStore()?.isAction`). - Instead of adding a new AsyncLocalStorage, use an empty string sentinel in the digest when no explicit type is provided. The action handler resolves empty to `"push"`, while SSR render contexts only extract `location`/`statusCode` (no type needed at HTTP level). - Also adds an optional `type` parameter to `permanentRedirect()` matching the Next.js signature (defaults to `"replace"`). ### How it works 1. `redirect("/foo")` now produces digest `NEXT_REDIRECT;;%2Ffoo` (empty type field) 2. `redirect("/foo", "push")` produces `NEXT_REDIRECT;push;%2Ffoo` (explicit, always respected) 3. The action handler in `app-rsc-entry.ts` resolves empty to `"push"` (`parts[1] || "push"`) 4. The browser entry reads `x-action-redirect-type` header and uses `window.location.assign()` for push / `window.location.replace()` for replace ### Next.js reference - [`packages/next/src/client/components/redirect.ts`](https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/redirect.ts) — line 43: `type ??= actionAsyncStorage?.getStore()?.isAction ? 'push' : 'replace'` - [`packages/next/src/client/components/redirect.ts`](https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/redirect.ts) — line 62: `permanentRedirect` accepts optional `type` parameter ## Test plan - [x] `redirect()` without explicit type produces empty sentinel in digest - [x] `redirect()` with explicit `"push"` or `"replace"` preserves type in digest - [x] Semicolon-in-URL encoding still prevents digest injection (updated assertion) - [x] `permanentRedirect()` accepts optional `type` parameter - [x] `permanentRedirect()` defaults to `"replace"` when no type given - [x] Generated action handler defaults empty type to `"push"` (template assertion) - [x] Entry template snapshots updated - [x] All existing redirect/error-boundary/route-handler tests pass --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:10:24 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#839
No description provided.