Skip to content

Commit

Permalink
Merge branch 'canary' into not-found-default
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] authored Aug 28, 2023
2 parents 80bf6b1 + a6a452b commit e5b1715
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 19 deletions.
4 changes: 2 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
packages/next/bundles/** -text
packages/next/compiled/** -text
packages/next/bundles/** -text linguist-vendored
packages/next/compiled/** -text linguist-vendored

# Make next/src/build folder indexable for github search
build/** linguist-generated=false
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 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.
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 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:
Expand Down
28 changes: 23 additions & 5 deletions docs/02-app/02-api-reference/04-functions/revalidatePath.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,22 @@ export default async function submit() {
### Route Handler

```ts filename="app/api/revalidate/route.ts" switcher
import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
const path = request.nextUrl.searchParams.get('path')
revalidatePath(path)
return NextResponse.json({ revalidated: true, now: Date.now() })

if (path) {
revalidatePath(path)
return NextResponse.json({ revalidated: true, now: Date.now() })
}

return NextResponse.json({
revalidated: false,
now: Date.now(),
message: 'Missing path to revalidate',
})
}
```

Expand All @@ -57,7 +66,16 @@ import { revalidatePath } from 'next/cache'

export async function GET(request) {
const path = request.nextUrl.searchParams.get('path')
revalidatePath(path)
return NextResponse.json({ revalidated: true, now: Date.now() })

if (path) {
revalidatePath(path)
return NextResponse.json({ revalidated: true, now: Date.now() })
}

return NextResponse.json({
revalidated: false,
now: Date.now(),
message: 'Missing path to revalidate',
})
}
```
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
}
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'
36 changes: 27 additions & 9 deletions packages/next/src/client/components/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,17 +28,31 @@ 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
*/
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)
}

/**
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
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 @@ -48,6 +48,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'
Expand Down Expand Up @@ -1477,11 +1478,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}
/>
)
Expand Down Expand Up @@ -1561,7 +1564,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

0 comments on commit e5b1715

Please sign in to comment.