Add new permanentRedirect
function in App Router (#54047)
for internal: https://vercel.slack.com/archives/C03S8ED1DKM/p1691700057242999 ### Problem - The existing [`redirect()` function](https://nextjs.org/docs/app/api-reference/functions/redirect) can't control the status code. - The existing [`redirect()` function](https://nextjs.org/docs/app/api-reference/functions/redirect) returns a 307 HTTP redirect response while it returns a 308-equivalent meta tag `<meta http-equiv="refresh" content="0;url=/foo"/>` in streaming response (e.g., suspense boundary), making the behavior inconsistent. ### Solution Adding a new `permanentRedirect()` function and changing the meta tag default accordingly. | func | HTTP status | meta tag | |---|:---:|---| | `redirect()` | 307 | `<meta http-equiv="refresh" content="1;url=/foo"/>` | | `permanentRedirect()` | 308 | `<meta http-equiv="refresh" content="0;url=/foo"/>` | ref. https://developers.google.com/search/docs/crawling-indexing/301-redirects --------- Co-authored-by: JJ Kasper <jj@jjsweb.site> Co-authored-by: Tim Neutkens <tim@timneutkens.nl> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
bdfbde5db8
commit
efd8d22654
9 changed files with 166 additions and 12 deletions
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
title: permanentRedirect
|
||||
description: API Reference for the permanentRedirect function.
|
||||
---
|
||||
|
||||
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/building-your-application/routing/route-handlers), and [Server Actions](/docs/app/building-your-application/data-fetching/forms-and-mutations).
|
||||
|
||||
When used in a streaming context, this will insert a meta tag to emit the redirect on the client side. Otherwise, it will serve a 308 (Permanent) HTTP redirect response to the caller.
|
||||
|
||||
If a resource doesn't exist, you can use the [`notFound` function](/docs/app/api-reference/functions/not-found) instead.
|
||||
|
||||
> **Good to know**: If you prefer to return a 307 (Temporary) HTTP redirect instead of 308 (Permanent), you can use the [`redirect` function](/docs/app/api-reference/functions/redirect) instead.
|
||||
|
||||
## Parameters
|
||||
|
||||
The `permanentRedirect` function accepts two arguments:
|
||||
|
||||
```js
|
||||
permanentRedirect(path, type)
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| `path` | `string` | The URL to redirect to. Can be a relative or absolute path. |
|
||||
| `type` | `'replace'` (default) or `'push'` (default in Server Actions) | The type of redirect to perform. |
|
||||
|
||||
By default, `permanentRedirect` will use `push` (adding a new entry to the browser history stack) in [Server Actions](/docs/app/building-your-application/data-fetching/forms-and-mutations) and `replace` (replacing the current URL in the browser history stack) everywhere else. You can override this behavior by specifying the `type` parameter.
|
||||
|
||||
The `type` parameter has no effect when used in Server Components.
|
||||
|
||||
## Returns
|
||||
|
||||
`permanentRedirect` does not return any value.
|
||||
|
||||
## Example
|
||||
|
||||
Invoking the `permanentRedirect()` function throws a `NEXT_REDIRECT` error and terminates rendering of the route segment in which it was thrown.
|
||||
|
||||
```jsx filename="app/team/[id]/page.js"
|
||||
import { permanentRedirect } from 'next/navigation'
|
||||
|
||||
async function fetchTeam(id) {
|
||||
const res = await fetch('https://...')
|
||||
if (!res.ok) return undefined
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export default async function Profile({ params }) {
|
||||
const team = await fetchTeam(params.id)
|
||||
if (!team) {
|
||||
permanentRedirect('/login')
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
> **Good to know**: `permanentRedirect` does not require you to use `return permanentRedirect()` as it uses the TypeScript [`never`](https://www.typescriptlang.org/docs/handbook/2/functions.html#never) type.
|
|
@ -5,8 +5,12 @@ description: API Reference for the redirect function.
|
|||
|
||||
The `redirect` function allows you to redirect the user to another URL. `redirect` can be used in Server Components, Client Components, [Route Handlers](/docs/app/building-your-application/routing/route-handlers), and [Server Actions](/docs/app/building-your-application/data-fetching/forms-and-mutations).
|
||||
|
||||
When used in a streaming context, this will insert a meta tag to emit the redirect on the client side. Otherwise, it will serve a 307 HTTP redirect response to the caller.
|
||||
|
||||
If a resource doesn't exist, you can use the [`notFound` function](/docs/app/api-reference/functions/not-found) instead.
|
||||
|
||||
> **Good to know**: If you prefer to return a 308 (Permanent) HTTP redirect instead of 307 (Temporary), you can use the [`permanentRedirect` function](/docs/app/api-reference/functions/permanentRedirect) instead.
|
||||
|
||||
## Parameters
|
||||
|
||||
The `redirect` function accepts two arguments:
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { type RedirectError, isRedirectError } from './redirect'
|
||||
|
||||
export function getRedirectStatusCodeFromError<U extends string>(
|
||||
error: RedirectError<U>
|
||||
): number {
|
||||
if (!isRedirectError(error)) {
|
||||
throw new Error('Not a redirect error')
|
||||
}
|
||||
|
||||
return error.digest.split(';', 4)[3] === 'true' ? 308 : 307
|
||||
}
|
|
@ -238,5 +238,5 @@ export function useSelectedLayoutSegment(
|
|||
return selectedLayoutSegments[0]
|
||||
}
|
||||
|
||||
export { redirect } from './redirect'
|
||||
export { redirect, permanentRedirect } from './redirect'
|
||||
export { notFound } from './not-found'
|
||||
|
|
|
@ -8,17 +8,18 @@ export enum RedirectType {
|
|||
replace = 'replace',
|
||||
}
|
||||
|
||||
type RedirectError<U extends string> = Error & {
|
||||
digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${U}`
|
||||
export type RedirectError<U extends string> = Error & {
|
||||
digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${U};${boolean}`
|
||||
mutableCookies: ResponseCookies
|
||||
}
|
||||
|
||||
export function getRedirectError(
|
||||
url: string,
|
||||
type: RedirectType
|
||||
type: RedirectType,
|
||||
permanent: boolean = false
|
||||
): RedirectError<typeof url> {
|
||||
const error = new Error(REDIRECT_ERROR_CODE) as RedirectError<typeof url>
|
||||
error.digest = `${REDIRECT_ERROR_CODE};${type};${url}`
|
||||
error.digest = `${REDIRECT_ERROR_CODE};${type};${url};${permanent}`
|
||||
const requestStore = requestAsyncStorage.getStore()
|
||||
if (requestStore) {
|
||||
error.mutableCookies = requestStore.mutableCookies
|
||||
|
@ -27,9 +28,9 @@ export function getRedirectError(
|
|||
}
|
||||
|
||||
/**
|
||||
* When used in a React server component, this will insert a meta tag to
|
||||
* When used in a streaming context, this will insert a meta tag to
|
||||
* redirect the user to the target page. When used in a custom app route, it
|
||||
* will serve a 302 to the caller.
|
||||
* will serve a 307 to the caller.
|
||||
*
|
||||
* @param url the url to redirect to
|
||||
*/
|
||||
|
@ -37,7 +38,21 @@ export function redirect(
|
|||
url: string,
|
||||
type: RedirectType = RedirectType.replace
|
||||
): never {
|
||||
throw getRedirectError(url, type)
|
||||
throw getRedirectError(url, type, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* When used in a streaming context, this will insert a meta tag to
|
||||
* redirect the user to the target page. When used in a custom app route, it
|
||||
* will serve a 308 to the caller.
|
||||
*
|
||||
* @param url the url to redirect to
|
||||
*/
|
||||
export function permanentRedirect(
|
||||
url: string,
|
||||
type: RedirectType = RedirectType.replace
|
||||
): never {
|
||||
throw getRedirectError(url, type, true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,12 +67,15 @@ export function isRedirectError<U extends string>(
|
|||
): error is RedirectError<U> {
|
||||
if (typeof error?.digest !== 'string') return false
|
||||
|
||||
const [errorCode, type, destination] = (error.digest as string).split(';', 3)
|
||||
const [errorCode, type, destination, permanent] = (
|
||||
error.digest as string
|
||||
).split(';', 4)
|
||||
|
||||
return (
|
||||
errorCode === REDIRECT_ERROR_CODE &&
|
||||
(type === 'replace' || type === 'push') &&
|
||||
typeof destination === 'string'
|
||||
typeof destination === 'string' &&
|
||||
(permanent === 'true' || permanent === 'false')
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ import {
|
|||
getURLFromRedirectError,
|
||||
isRedirectError,
|
||||
} from '../../client/components/redirect'
|
||||
import { getRedirectStatusCodeFromError } from '../../client/components/get-redirect-status-code-from-error'
|
||||
import { addImplicitTags, patchFetch } from '../lib/patch-fetch'
|
||||
import { AppRenderSpan } from '../lib/trace/constants'
|
||||
import { getTracer } from '../lib/trace/tracer'
|
||||
|
@ -1478,11 +1479,13 @@ export async function renderToHTMLOrFlight(
|
|||
)
|
||||
} else if (isRedirectError(error)) {
|
||||
const redirectUrl = getURLFromRedirectError(error)
|
||||
const isPermanent =
|
||||
getRedirectStatusCodeFromError(error) === 308 ? true : false
|
||||
if (redirectUrl) {
|
||||
errorMetaTags.push(
|
||||
<meta
|
||||
httpEquiv="refresh"
|
||||
content={`0;url=${redirectUrl}`}
|
||||
content={`${isPermanent ? 0 : 1};url=${redirectUrl}`}
|
||||
key={error.digest}
|
||||
/>
|
||||
)
|
||||
|
@ -1562,7 +1565,7 @@ export async function renderToHTMLOrFlight(
|
|||
let hasRedirectError = false
|
||||
if (isRedirectError(err)) {
|
||||
hasRedirectError = true
|
||||
res.statusCode = 307
|
||||
res.statusCode = getRedirectStatusCodeFromError(err)
|
||||
if (err.mutableCookies) {
|
||||
const headers = new Headers()
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { permanentRedirect } from 'next/navigation'
|
||||
|
||||
export default function Page() {
|
||||
permanentRedirect('/redirect/result')
|
||||
return <></>
|
||||
}
|
41
test/e2e/app-dir/navigation/app/redirect/suspense-2/page.js
Normal file
41
test/e2e/app-dir/navigation/app/redirect/suspense-2/page.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { Suspense } from 'react'
|
||||
import { permanentRedirect } from 'next/navigation'
|
||||
|
||||
function createSuspenseyComponent(Component, { timeout = 0, expire = 10 }) {
|
||||
let result
|
||||
let promise
|
||||
return function Data() {
|
||||
if (result) return result
|
||||
if (!promise)
|
||||
promise = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
result = <Component />
|
||||
setTimeout(() => {
|
||||
result = undefined
|
||||
promise = undefined
|
||||
}, expire)
|
||||
resolve()
|
||||
}, timeout)
|
||||
})
|
||||
throw promise
|
||||
}
|
||||
}
|
||||
|
||||
function Redirect() {
|
||||
permanentRedirect('/redirect/result')
|
||||
return <></>
|
||||
}
|
||||
|
||||
const SuspenseyRedirect = createSuspenseyComponent(Redirect, {
|
||||
timeout: 300,
|
||||
})
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<div className="suspense">
|
||||
<Suspense fallback="fallback">
|
||||
<SuspenseyRedirect />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -421,6 +421,12 @@ createNextDescribe(
|
|||
})
|
||||
expect(res.status).toBe(307)
|
||||
})
|
||||
it('should respond with 308 status code if permanent flag is set', async () => {
|
||||
const res = await next.fetch('/redirect/servercomponent-2', {
|
||||
redirect: 'manual',
|
||||
})
|
||||
expect(res.status).toBe(308)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -551,6 +557,13 @@ createNextDescribe(
|
|||
|
||||
it('should emit refresh meta tag for redirect page when streaming', async () => {
|
||||
const html = await next.render('/redirect/suspense')
|
||||
expect(html).toContain(
|
||||
'<meta http-equiv="refresh" content="1;url=/redirect/result"/>'
|
||||
)
|
||||
})
|
||||
|
||||
it('should emit refresh meta tag (peramnent) for redirect page when streaming', async () => {
|
||||
const html = await next.render('/redirect/suspense-2')
|
||||
expect(html).toContain(
|
||||
'<meta http-equiv="refresh" content="0;url=/redirect/result"/>'
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue