Return 200 for client-handled Server Action redirects (#96310)

## Summary

Client-handled Server Action redirects currently return a `303` without
a `Location` header and may include a streamed RSC response body. Some
HTTP intermediaries reject this response or strip its body because it
looks like an HTTP redirect.

Return `200` for client-handled Server Action redirects while continuing
to use `x-action-redirect` and the RSC response. Progressive-enhancement
form submissions still return a standards-compliant `303` with
`Location`.

Fixes #92882
Fixes #74026

## Verification

- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/actions/app-action.test.ts
test/e2e/app-dir/app-basepath/index.test.ts
test/e2e/app-dir/actions/app-action-progressive-enhancement.test.ts`
- `HEADLESS=true pnpm test-start-turbo
test/e2e/app-dir/actions/app-action.test.ts
test/e2e/app-dir/app-basepath/index.test.ts
test/e2e/app-dir/actions/app-action-progressive-enhancement.test.ts`
- `pnpm --filter=next types`

<!-- NEXT_JS_LLM -->
This commit is contained in:
Zack Tanner
2026-07-28 16:04:34 -04:00
committed by GitHub
parent a28e346a1a
commit ccfe67aa8d
9 changed files with 44 additions and 19 deletions
+2 -2
View File
@@ -80,7 +80,7 @@ export async function createPost(id) {
> **Good to know**:
>
> - `redirect` returns a 307 (Temporary Redirect) status code by default. When used in a Server Action, it returns a 303 (See Other), which is commonly used for redirecting to a success page as a result of a POST request.
> - In a Server Action, `redirect` performs a client-side navigation when JavaScript is available. Without JavaScript, the form submission uses a 303 (See Other). In other contexts, `redirect` uses a 307 (Temporary Redirect).
> - `redirect` throws an error so it should be called **outside** the `try` block when using `try/catch` statements.
> - `redirect` can be called in Client Components during the rendering process but not in event handlers. You can use the [`useRouter` hook](#userouter-hook) instead.
> - `redirect` also accepts absolute URLs and can be used to redirect to external links.
@@ -132,7 +132,7 @@ export async function updateUsername(username, formData) {
> **Good to know**:
>
> - `permanentRedirect` returns a 308 (permanent redirect) status code by default.
> - In a Server Action, `permanentRedirect` performs a client-side navigation when JavaScript is available. Without JavaScript, the form submission uses a 303 (See Other). In other contexts, `permanentRedirect` uses a 308 (Permanent Redirect).
> - `permanentRedirect` also accepts absolute URLs and can be used to redirect to external links.
> - If you'd like to redirect before the render process, use [`next.config.js`](#redirects-in-nextconfigjs) or [Proxy](#nextresponseredirect-in-proxy).
@@ -8,7 +8,7 @@ related:
The `permanentRedirect` function allows you to redirect the user to another URL. `permanentRedirect` can be used in Server Components, Client Components, [Route Handlers](/docs/app/api-reference/file-conventions/route), and [Server Functions](/docs/app/getting-started/mutating-data).
When used in a streaming context, this will insert a meta tag to emit the redirect on the client side. When used in a server action, it will serve a 303 HTTP redirect response to the caller. Otherwise, it will serve a 308 (Permanent) HTTP redirect response to the caller.
When used in a streaming context, this will insert a meta tag to emit the redirect on the client side. In a Server Action, `permanentRedirect` performs a client-side navigation when JavaScript is available. For progressive enhancement form submissions, it serves a 303 HTTP redirect response. Otherwise, it serves a 308 (Permanent) HTTP redirect response.
If a resource doesn't exist, you can use the [`notFound` function](/docs/app/api-reference/functions/not-found) instead.
@@ -9,7 +9,7 @@ related:
The `redirect` function allows you to redirect the user to another URL. `redirect` can be used while rendering in [Server and Client Components](/docs/app/getting-started/server-and-client-components), [Route Handlers](/docs/app/api-reference/file-conventions/route), and [Server Functions](/docs/app/getting-started/mutating-data).
When used in a [streaming context](/docs/app/getting-started/linking-and-navigating#streaming), this will insert a meta tag to emit the redirect on the client side. When used in a server action, it will serve a 303 HTTP redirect response to the caller. Otherwise, it will serve a 307 HTTP redirect response to the caller.
When used in a [streaming context](/docs/app/getting-started/linking-and-navigating#streaming), this will insert a meta tag to emit the redirect on the client side. In a Server Action, `redirect` performs a client-side navigation when JavaScript is available. For progressive enhancement form submissions, it serves a 303 HTTP redirect response. Otherwise, it serves a 307 HTTP redirect response.
If a resource doesn't exist, you can use the [`notFound` function](/docs/app/api-reference/functions/not-found) instead.
@@ -212,7 +212,7 @@ The introduction of the `307` status code means that the request method is prese
- `302` - Temporary redirect, will change the request method from `POST` to `GET`
- `307` - Temporary redirect, will preserve the request method as `POST`
The `redirect()` method uses a `307` by default, instead of a `302` temporary redirect, meaning your requests will _always_ be preserved as `POST` requests.
The `redirect()` method uses a `307` by default, instead of a `302` temporary redirect, meaning the request method is preserved. Server Action form submissions are an exception: they use a `303` response so the browser follows the redirect with a `GET` request. When JavaScript is available, Server Actions perform a client-side navigation instead of an HTTP redirect.
[Learn more](https://developer.mozilla.org/docs/Web/HTTP/Redirections) about HTTP Redirects.
@@ -24,7 +24,8 @@ export function getRedirectError(
* [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations).
*
* - In a Server Component, this will insert a meta tag to redirect the user to the target page.
* - In a Route Handler or Server Action, it will serve a 307/303 to the caller.
* - In a Route Handler, it will serve a 307 to the caller.
* - In a Server Action, it will perform a client-side navigation when JavaScript is available or serve a 303 for a progressive enhancement form submission.
* - In a Server Action, type defaults to 'push' and 'replace' elsewhere.
*
* Read more: [Next.js Docs: `redirect`](https://nextjs.org/docs/app/api-reference/functions/redirect)
@@ -46,7 +47,8 @@ export function redirect(
* [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations).
*
* - In a Server Component, this will insert a meta tag to redirect the user to the target page.
* - In a Route Handler or Server Action, it will serve a 308/303 to the caller.
* - In a Route Handler, it will serve a 308 to the caller.
* - In a Server Action, it will perform a client-side navigation when JavaScript is available or serve a 303 for a progressive enhancement form submission.
*
* Read more: [Next.js Docs: `redirect`](https://nextjs.org/docs/app/api-reference/functions/redirect)
*/
@@ -1272,12 +1272,13 @@ export async function handleAction({
const redirectUrl = getURLFromRedirectError(err)
const redirectType = getRedirectTypeFromError(err)
// if it's a fetch action, we'll set the status code for logging/debugging purposes
// but we won't set a Location header, as the redirect will be handled by the client router
res.statusCode = RedirectStatusCode.SeeOther
metadata.statusCode = RedirectStatusCode.SeeOther
if (isFetchAction) {
// Fetch actions communicate redirects through `x-action-redirect` and
// can include the redirect target's Flight response in the body. Since
// this is not an HTTP redirect, keep the response successful.
res.statusCode = 200
metadata.statusCode = 200
return {
type: 'done',
result: await createRedirectRenderResult(
@@ -1294,6 +1295,8 @@ export async function handleAction({
}
// For an MPA action, the redirect doesn't need a body, just a Location header.
res.statusCode = RedirectStatusCode.SeeOther
metadata.statusCode = RedirectStatusCode.SeeOther
res.setHeader('Location', redirectUrl)
return {
type: 'done',
@@ -378,7 +378,8 @@ declare module 'next/navigation' {
* [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations).
*
* - In a Server Component, this will insert a meta tag to redirect the user to the target page.
* - In a Route Handler or Server Action, it will serve a 307/303 to the caller.
* - In a Route Handler, it will serve a 307 to the caller.
* - In a Server Action, it will perform a client-side navigation when JavaScript is available or serve a 303 for a progressive enhancement form submission.
* - In a Server Action, type defaults to 'push' and 'replace' elsewhere.
*
* Read more: [Next.js Docs: redirect](https://nextjs.org/docs/app/api-reference/functions/redirect)
@@ -396,7 +397,8 @@ declare module 'next/navigation' {
* [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations).
*
* - In a Server Component, this will insert a meta tag to redirect the user to the target page.
* - In a Route Handler or Server Action, it will serve a 308/303 to the caller.
* - In a Route Handler, it will serve a 308 to the caller.
* - In a Server Action, it will perform a client-side navigation when JavaScript is available or serve a 303 for a progressive enhancement form submission.
*
* Read more: [Next.js Docs: redirect](https://nextjs.org/docs/app/api-reference/functions/redirect)
*/
+12 -2
View File
@@ -1185,7 +1185,12 @@ describe('app-dir action handling', () => {
expect(request.url()).toEqual(`${next.url}${initialPagePath}`)
expect(request.method()).toEqual('POST')
expect(response.status()).toEqual(303)
expect(response.status()).toEqual(200)
const headers = await response.allHeaders()
expect(headers['x-action-redirect']).toBeDefined()
expect(headers.location).toBeUndefined()
expect(headers['content-type']).toContain('text/x-component')
}
)
@@ -1298,7 +1303,12 @@ describe('app-dir action handling', () => {
expect(request.url()).toEqual(`${next.url}${initialPagePath}`)
expect(request.method()).toEqual('POST')
expect(response.status()).toEqual(303)
expect(response.status()).toEqual(200)
const headers = await response.allHeaders()
expect(headers['x-action-redirect']).toBeDefined()
expect(headers.location).toBeUndefined()
expect(headers['content-type']).toContain('text/x-component')
}
)
+10 -2
View File
@@ -156,7 +156,12 @@ describe('app dir - basepath', () => {
expect(request.url()).toEqual(`${next.url}${initialPagePath}`)
expect(request.method()).toEqual('POST')
expect(response.status()).toEqual(303)
expect(response.status()).toEqual(200)
const headers = await response.allHeaders()
expect(headers['x-action-redirect']).toBeDefined()
expect(headers.location).toBeUndefined()
expect(headers['content-type']).toContain('text/x-component')
}
)
@@ -204,7 +209,10 @@ describe('app dir - basepath', () => {
expect(secondRequest.url()).toEqual(`${next.url}${destinationPagePath}`)
expect(secondRequest.method()).toEqual('GET')
expect(firstResponse.status()).toEqual(303)
expect(firstResponse.status()).toEqual(200)
const headers = await firstResponse.allHeaders()
expect(headers['x-action-redirect']).toBeDefined()
expect(headers.location).toBeUndefined()
// Since this is an external request to a resource outside of NextJS
// we expect to see a separate request resolving the external URL.
expect(secondResponse.status()).toEqual(200)
@@ -13,7 +13,7 @@ export async function multiArgAction(a, b, c) {
return { sum: a + b + c }
}
// Action that redirects (should show 303 status)
// Action that redirects
export async function redirectAction(path) {
redirect(path)
}