mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
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:
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user