Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new permanentRedirect function in App Router #54047

Merged
merged 17 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/02-app/02-api-reference/04-functions/permanentRedirect.mdx
Original file line number Diff line number Diff line change
@@ -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 React Server Component (e.g., with a `<Suspense>` boundary), 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.
4 changes: 4 additions & 0 deletions docs/02-app/02-api-reference/04-functions/redirect.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 React Server Component (e.g., with a `<Suspense>` boundary), 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.
smaeda-ks marked this conversation as resolved.
Show resolved Hide resolved

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:
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,5 +238,5 @@ export function useSelectedLayoutSegment(
return selectedLayoutSegments[0]
}

export { redirect } from './redirect'
export { redirect, permanentRedirect } from './redirect'
export { notFound } from './not-found'
42 changes: 35 additions & 7 deletions packages/next/src/client/components/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ export enum RedirectType {
}

type RedirectError<U extends string> = Error & {
digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${U}`
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
Expand All @@ -29,15 +30,29 @@ export function getRedirectError(
/**
* When used in a React server component, 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
*/
export function redirect(
url: string,
type: RedirectType = RedirectType.replace
): never {
throw getRedirectError(url, type)
throw getRedirectError(url, type, false)
}

/**
* When used in a React server component, 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)
}

/**
Expand All @@ -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')
)
}

Expand Down Expand Up @@ -88,3 +106,13 @@ export function getRedirectTypeFromError<U extends string>(

return error.digest.split(';', 3)[1] as RedirectType
}

export function getRedirectStatusCodeFromError<U extends string>(
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
error: RedirectError<U>
): number {
if (!isRedirectError(error)) {
throw new Error('Not a redirect error')
}

return error.digest.split(';', 4)[3] === 'true' ? 308 : 307
}
7 changes: 5 additions & 2 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { getLayoutOrPageModule, LoaderTree } from '../lib/app-dir-module'
import { isNotFoundError } from '../../client/components/not-found'
import {
getURLFromRedirectError,
getRedirectStatusCodeFromError,
isRedirectError,
} from '../../client/components/redirect'
import { addImplicitTags, patchFetch } from '../lib/patch-fetch'
Expand Down Expand Up @@ -1478,11 +1479,13 @@ export async function renderToHTMLOrFlight(
)
} else if (isRedirectError(error)) {
const redirectUrl = getURLFromRedirectError(error)
const isPermanet =
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
getRedirectStatusCodeFromError(error) === 308 ? true : false
if (redirectUrl) {
errorMetaTags.push(
<meta
httpEquiv="refresh"
content={`0;url=${redirectUrl}`}
content={`${isPermanet ? 0 : 1};url=${redirectUrl}`}
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
key={error.digest}
/>
)
Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { permanentRedirect } from 'next/navigation'

export default function Page() {
permanentRedirect('/redirect/result')
return <></>
}
41 changes: 41 additions & 0 deletions test/e2e/app-dir/navigation/app/redirect/suspense-2/page.js
Original file line number Diff line number Diff line change
@@ -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>
)
}
13 changes: 13 additions & 0 deletions test/e2e/app-dir/navigation/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})

Expand Down Expand Up @@ -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"/>'
)
Expand Down
Loading