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