-
Notifications
You must be signed in to change notification settings - Fork 26.7k
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
ztanner
merged 17 commits into
vercel:canary
from
smaeda-ks:shohei/add-permanent-redirect-option
Aug 28, 2023
+166
−12
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
02cba23
Add `permanent` redirect option
smaeda-ks 4df938e
Merge branch 'canary' into shohei/add-permanent-redirect-option
smaeda-ks 8d28f91
add tests
smaeda-ks 6053d9c
use rest parameters
smaeda-ks 35d09bd
Merge branch 'canary' into shohei/add-permanent-redirect-option
ijjk 3c8dd7c
update docs
smaeda-ks 8f90d05
Revert "update docs"
smaeda-ks 6a9f872
Add separate API `permanentRedirect`
smaeda-ks f9dfb3c
update docs
smaeda-ks 87a8f40
Update permanentRedirect.mdx
smaeda-ks dbe7e48
update docs
smaeda-ks 151a206
Merge branch 'canary' into shohei/add-permanent-redirect-option
smaeda-ks 7344495
fix links
smaeda-ks d4261c5
Merge branch 'shohei/add-permanent-redirect-option' of https://github…
smaeda-ks 4880178
address feedback
smaeda-ks b315c11
Apply suggestions from code review
timneutkens 0d73245
Merge branch 'canary' into shohei/add-permanent-redirect-option
kodiakhq[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
docs/02-app/02-api-reference/04-functions/permanentRedirect.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
packages/next/src/client/components/get-redirect-status-code-from-error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
test/e2e/app-dir/navigation/app/redirect/servercomponent-2/page.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
41
test/e2e/app-dir/navigation/app/redirect/suspense-2/page.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I understand setting this number to 1 means the browser will wait 1 second before redirecting, are you aware of that?
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intentional. Googlebot considers
0
to be permanent and anything above 0 to be temporary: https://developers.google.com/search/docs/crawling-indexing/301-redirectsIt's an SEO thing basically, unfortunately Googlebot doesn't support
http-equiv
status
which would allow us to do this while preserving instant redirect.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1s delay downsides with
redirect()
(maybe this should be its own issue/discussion)Hm, the 1s delay on
redirect()
is not ideal for auth gating (temporaryredirect()
in either inpage.tsx
or inlayout.tsx
):permanentRedirect()
would not be correct semantically (a permanent redirect from a page to/login
not correct)Ideally, one of the following seems like more well-rounded solutions without the UX downsides:
A) Middleware TCP support
B) Setting a
Location
response header whenredirect()
orpermanentRedirect()
are used - in a style similar to how React Float changes other parts of tree / responseBut I guess for now we will consider recommending
permanentRedirect()
to our students for now to avoid the 1s weirdness (tradeoff against the semantically-incorrect "permanent" part)If a
<meta>
tag is the only thing that can be used for now, I guess the previous solution of 307 + 0s refresh feels at first glance like a better tradeoff... (at least the status code is correct in that case and it doesn't promote a slow UX)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A simple solution would be to redirect from JavaScript if there is the refresh meta tag so you don’t need to wait the 1 second delay, not sure if the router already does this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tricky part is around streaming in general. If it's used in a streaming context and HTTP headers are already sent to the client (e.g., with status 200), we can't emit additional headers afterward. This is why meta tag is used this way as far as I understand.
Google warns Javascript redirect to be the last resort to adopt so I guess it's safe to avoid that:
https://developers.google.com/search/docs/crawling-indexing/301-redirects#jslocation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, with my suggestion B above I meant to "lift up" the
redirect()
/permanentRedirect()
and logic to an earlier part in the response cycle, so that both the status code and theLocation
header could be setThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The moment your components run you can't assume the headers haven't been sent yet. In the near future there will be early hints / preloads that means even before your code runs it's already streaming.
Redirecting for authentication should be handled in middleware if you want it to run in front of any rendering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use pages / layouts auth gating as also recommended as an alternative by Lee - not as a choice, but as a necessity.
We do this because Middleware is not an option. The Edge Runtime Middleware is not capable of doing auth gating with database auth, if you need a TCP connection, which most database options do (workaround edge database products exist, such as Vercel Postgres / Neon, but not everyone can use them). So these users of Next.js App Router are currently stuck with pages / layout auth gating.
However, I would love to be able to do this with Middleware, eg. if there will be tangible movement soon about Node.js Runtime Middleware as mentioned by Lee: