From bdfbde5db867e83cb805af45c50ca77879655d1f Mon Sep 17 00:00:00 2001
From: Krychaxp <55843050+krychaxp@users.noreply.github.com>
Date: Mon, 28 Aug 2023 22:33:49 +0200
Subject: [PATCH 1/3] Update revalidatePath.mdx (#54631)
add if statement, because typescript throws error: revalidatePath require `string`, but `searchParams.get('path')` returns `string|null` type
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
---
.../04-functions/revalidatePath.mdx | 28 +++++++++++++++----
1 file changed, 23 insertions(+), 5 deletions(-)
diff --git a/docs/02-app/02-api-reference/04-functions/revalidatePath.mdx b/docs/02-app/02-api-reference/04-functions/revalidatePath.mdx
index 95fb76db826ad..abc0b6c9f6f7d 100644
--- a/docs/02-app/02-api-reference/04-functions/revalidatePath.mdx
+++ b/docs/02-app/02-api-reference/04-functions/revalidatePath.mdx
@@ -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',
+ })
}
```
@@ -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',
+ })
}
```
From efd8d2265475f37270c8c0d293bc6b5b467eaa4b Mon Sep 17 00:00:00 2001
From: Shohei Maeda <11495867+smaeda-ks@users.noreply.github.com>
Date: Tue, 29 Aug 2023 06:22:43 +0900
Subject: [PATCH 2/3] 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 `` 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 | `` |
| `permanentRedirect()` | 308 | `` |
ref.
https://developers.google.com/search/docs/crawling-indexing/301-redirects
---------
Co-authored-by: JJ Kasper
Co-authored-by: Tim Neutkens
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
---
.../04-functions/permanentRedirect.mdx | 58 +++++++++++++++++++
.../04-functions/redirect.mdx | 4 ++
.../get-redirect-status-code-from-error.ts | 11 ++++
.../next/src/client/components/navigation.ts | 2 +-
.../next/src/client/components/redirect.ts | 36 +++++++++---
.../next/src/server/app-render/app-render.tsx | 7 ++-
.../app/redirect/servercomponent-2/page.js | 6 ++
.../app/redirect/suspense-2/page.js | 41 +++++++++++++
.../e2e/app-dir/navigation/navigation.test.ts | 13 +++++
9 files changed, 166 insertions(+), 12 deletions(-)
create mode 100644 docs/02-app/02-api-reference/04-functions/permanentRedirect.mdx
create mode 100644 packages/next/src/client/components/get-redirect-status-code-from-error.ts
create mode 100644 test/e2e/app-dir/navigation/app/redirect/servercomponent-2/page.js
create mode 100644 test/e2e/app-dir/navigation/app/redirect/suspense-2/page.js
diff --git a/docs/02-app/02-api-reference/04-functions/permanentRedirect.mdx b/docs/02-app/02-api-reference/04-functions/permanentRedirect.mdx
new file mode 100644
index 0000000000000..3162d2a8c7027
--- /dev/null
+++ b/docs/02-app/02-api-reference/04-functions/permanentRedirect.mdx
@@ -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.
diff --git a/docs/02-app/02-api-reference/04-functions/redirect.mdx b/docs/02-app/02-api-reference/04-functions/redirect.mdx
index 9bc5ddb73d8c6..eb3a1337a4247 100644
--- a/docs/02-app/02-api-reference/04-functions/redirect.mdx
+++ b/docs/02-app/02-api-reference/04-functions/redirect.mdx
@@ -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:
diff --git a/packages/next/src/client/components/get-redirect-status-code-from-error.ts b/packages/next/src/client/components/get-redirect-status-code-from-error.ts
new file mode 100644
index 0000000000000..44e89ea751570
--- /dev/null
+++ b/packages/next/src/client/components/get-redirect-status-code-from-error.ts
@@ -0,0 +1,11 @@
+import { type RedirectError, isRedirectError } from './redirect'
+
+export function getRedirectStatusCodeFromError(
+ error: RedirectError
+): number {
+ if (!isRedirectError(error)) {
+ throw new Error('Not a redirect error')
+ }
+
+ return error.digest.split(';', 4)[3] === 'true' ? 308 : 307
+}
diff --git a/packages/next/src/client/components/navigation.ts b/packages/next/src/client/components/navigation.ts
index c4f977a2338a1..4c28fa60d058c 100644
--- a/packages/next/src/client/components/navigation.ts
+++ b/packages/next/src/client/components/navigation.ts
@@ -238,5 +238,5 @@ export function useSelectedLayoutSegment(
return selectedLayoutSegments[0]
}
-export { redirect } from './redirect'
+export { redirect, permanentRedirect } from './redirect'
export { notFound } from './not-found'
diff --git a/packages/next/src/client/components/redirect.ts b/packages/next/src/client/components/redirect.ts
index 5c002a8842277..10e72bc1ccbef 100644
--- a/packages/next/src/client/components/redirect.ts
+++ b/packages/next/src/client/components/redirect.ts
@@ -8,17 +8,18 @@ export enum RedirectType {
replace = 'replace',
}
-type RedirectError = Error & {
- digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${U}`
+export type RedirectError = Error & {
+ digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${U};${boolean}`
mutableCookies: ResponseCookies
}
export function getRedirectError(
url: string,
- type: RedirectType
+ type: RedirectType,
+ permanent: boolean = false
): RedirectError {
const error = new Error(REDIRECT_ERROR_CODE) as RedirectError
- 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(
): error is RedirectError {
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')
)
}
diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx
index 851634a17edfc..08d683e42348f 100644
--- a/packages/next/src/server/app-render/app-render.tsx
+++ b/packages/next/src/server/app-render/app-render.tsx
@@ -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(
)
@@ -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()
diff --git a/test/e2e/app-dir/navigation/app/redirect/servercomponent-2/page.js b/test/e2e/app-dir/navigation/app/redirect/servercomponent-2/page.js
new file mode 100644
index 0000000000000..524221bae5206
--- /dev/null
+++ b/test/e2e/app-dir/navigation/app/redirect/servercomponent-2/page.js
@@ -0,0 +1,6 @@
+import { permanentRedirect } from 'next/navigation'
+
+export default function Page() {
+ permanentRedirect('/redirect/result')
+ return <>>
+}
diff --git a/test/e2e/app-dir/navigation/app/redirect/suspense-2/page.js b/test/e2e/app-dir/navigation/app/redirect/suspense-2/page.js
new file mode 100644
index 0000000000000..d9388ce25a416
--- /dev/null
+++ b/test/e2e/app-dir/navigation/app/redirect/suspense-2/page.js
@@ -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 =
+ 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 (
+
+
+
+
+
+ )
+}
diff --git a/test/e2e/app-dir/navigation/navigation.test.ts b/test/e2e/app-dir/navigation/navigation.test.ts
index cda4360d12a8a..67beabf483fe3 100644
--- a/test/e2e/app-dir/navigation/navigation.test.ts
+++ b/test/e2e/app-dir/navigation/navigation.test.ts
@@ -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(
+ ''
+ )
+ })
+
+ it('should emit refresh meta tag (peramnent) for redirect page when streaming', async () => {
+ const html = await next.render('/redirect/suspense-2')
expect(html).toContain(
''
)
From a6a452b3b24fa27940b64f500b0cd2489a226081 Mon Sep 17 00:00:00 2001
From: Steven
Date: Mon, 28 Aug 2023 16:50:19 -0500
Subject: [PATCH 3/3] chore: update gitattributes with linguist-vendored
(#54683)
> Excluded from stats
https://github.com/github-linguist/linguist/blob/master/docs/overrides.md
---
.gitattributes | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.gitattributes b/.gitattributes
index 21621715952fa..5e53884779c0a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -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