From 6ab51f0f834a18d679d5ac13933d962cf3869211 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Wed, 10 Jul 2024 18:08:30 +0200 Subject: [PATCH 1/7] feat(eslint-plugin-next): Add 'no-redirect-in-try-catch' ESLint rule --- .../src/rules/no-redirect-in-try-catch.ts | 49 +++++++++++++ .../no-redirect-in-try-catch.test.ts | 71 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts create mode 100644 test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts diff --git a/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts b/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts new file mode 100644 index 0000000000000..d07a1a7fb399b --- /dev/null +++ b/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts @@ -0,0 +1,49 @@ +import { defineRule } from '../utils/define-rule' +const url = 'https://nextjs.org/docs/messages/no-redirect-in-try-catch' + +export = defineRule({ + meta: { + docs: { + description: 'Prevent usage of `redirect` in try-catch block.', + recommended: true, + url, + }, + type: 'problem', + schema: [], + }, + create(context) { + function checkForRedirectCall(statement) { + if ( + statement.type === 'ExpressionStatement' && + statement.expression.type === 'CallExpression' && + statement.expression.callee.name === 'redirect' + ) { + context.report({ + node: statement, + message: `Do not use \`redirect\` within a try-catch block. Move the \`redirect\` call outside of the try-catch block. See: ${url}`, + }) + } else if (statement.type === 'BlockStatement') { + statement.body.forEach((innerStatement) => { + checkForRedirectCall(innerStatement) + }) + } else if (statement.type === 'IfStatement') { + checkForRedirectCall(statement.consequent) + if (statement.alternate) { + checkForRedirectCall(statement.alternate) + } + } + } + + return { + ImportDeclaration(node) { + if (node.source.value !== 'next/navigation') { + return + } + }, + TryStatement(node) { + const tryBlockStatements = node.block.body + tryBlockStatements.forEach(checkForRedirectCall) + }, + } + }, +}) diff --git a/test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts b/test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts new file mode 100644 index 0000000000000..61f3982e38620 --- /dev/null +++ b/test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts @@ -0,0 +1,71 @@ +import rule from '@next/eslint-plugin-next/dist/rules/no-redirect-in-try-catch' +import { RuleTester } from 'eslint' +;(RuleTester as any).setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) +const ruleTester = new RuleTester() + +ruleTester.run('no-redirect-in-try-catch', rule, { + valid: [ + `'use server' + + import { redirect } from "next/navigation" + + export async function navigate(data) { + redirect(\`/posts/\${data.get('id')}\`) + }`, + ], + invalid: [ + { + code: ` + 'use server' + + import { redirect } from "next/navigation" + + export async function navigate(data) { + try { + redirect(\`/posts/\${data.get('id')}\`) + } catch (e) { + console.error(e); + } + }`, + filename: 'app/actions.ts', + errors: [ + { + message: + 'Do not use `redirect` within a try-catch block. Move the `redirect` call outside of the try-catch block. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', + }, + ], + }, + { + code: ` + 'use server' + + import { redirect } from "next/navigation" + + export async function navigate(data) { + try { + if (data.id) { + redirect(\`/posts/\${data.id}\`) + } + } catch (e) { + console.error(e); + } + }`, + filename: 'app/actions.ts', + errors: [ + { + message: + 'Do not use `redirect` within a try-catch block. Move the `redirect` call outside of the try-catch block. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', + }, + ], + }, + ], +}) From edd35c70164f5989e2a4f2b02034b3abd1c0ab0a Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Thu, 11 Jul 2024 12:50:15 +0200 Subject: [PATCH 2/7] docs(eslint): Add docs for no-redirect-in-try-catch rule --- .../07-configuring/02-eslint.mdx | 1 + errors/no-redirect-in-try-catch.mdx | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 errors/no-redirect-in-try-catch.mdx diff --git a/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx b/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx index e626340a09604..feb97d21f0e40 100644 --- a/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx +++ b/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx @@ -116,6 +116,7 @@ Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/ | | [@next/next/no-title-in-document-head](/docs/messages/no-title-in-document-head) | Prevent usage of `` with `Head` component from `next/document`. | | <Check size={18} /> | @next/next/no-typos | Prevent common typos in [Next.js's data fetching functions](/docs/pages/building-your-application/data-fetching) | | <Check size={18} /> | [@next/next/no-unwanted-polyfillio](/docs/messages/no-unwanted-polyfillio) | Prevent duplicate polyfills from Polyfill.io. | +| <Check size={18} /> | [@next/next/no-redirect-in-try-catch](/docs/messages/no-redirect-in-try-catch) | Prevent usage of `redirect` in try-catch block. | If you already have ESLint configured in your application, we recommend extending from this plugin directly instead of including `eslint-config-next` unless a few conditions are met. Refer to the [Recommended Plugin Ruleset](#recommended-plugin-ruleset) to learn more. diff --git a/errors/no-redirect-in-try-catch.mdx b/errors/no-redirect-in-try-catch.mdx new file mode 100644 index 0000000000000..1a9174194d109 --- /dev/null +++ b/errors/no-redirect-in-try-catch.mdx @@ -0,0 +1,43 @@ +--- +title: No Redirect in Try-Catch +--- + +> Prevent usage of `redirect` in try-catch block. + +## Why This Error Occurred + +You attempted to use the `redirect` function within a try-catch block. When `redirect` is called, it throws a `NEXT_REDIRECT` error internally. If this error is caught by the catch block, it prevents the redirect from executing as intended. + +## Possible Ways to Fix It + +Move the `redirect` call outside of the try-catch block. This ensures that the `NEXT_REDIRECT` error is not caught and the redirect can proceed without interference. + +## Example + +### Incorrect Usage: + +```javascript +try { + // some code that might throw an error + redirect('/some-path') +} catch (error) { + // handle error +} +``` + +### Correct Usage: + +```javascript +try { + // some code that might throw an error +} catch (error) { + // handle error +} +// Redirect outside of the try-catch block +redirect('/some-path') +``` + +## Useful Links + +- [Next.js Redirect Documentation](/docs/app/building-your-application/routing/redirecting#redirect-function) +- [Next.js Error Handling](/docs/app/building-your-application/routing/error-handling) From 95eef7bce49042b32e3702fccb8286b224ce5a98 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski <piotr.tomczewski@callstack.com> Date: Thu, 11 Jul 2024 12:50:36 +0200 Subject: [PATCH 3/7] feat(eslint-plugin-next): Add 'no-redirect-in-try-catch' rule to recommended config --- packages/eslint-plugin-next/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin-next/src/index.ts b/packages/eslint-plugin-next/src/index.ts index 78b8553f8f435..21d6adf966904 100644 --- a/packages/eslint-plugin-next/src/index.ts +++ b/packages/eslint-plugin-next/src/index.ts @@ -21,6 +21,7 @@ module.exports = { 'no-title-in-document-head': require('./rules/no-title-in-document-head'), 'no-typos': require('./rules/no-typos'), 'no-unwanted-polyfillio': require('./rules/no-unwanted-polyfillio'), + 'no-redirect-in-try-catch': require('./rules/no-redirect-in-try-catch'), }, configs: { recommended: { @@ -49,6 +50,7 @@ module.exports = { '@next/next/no-duplicate-head': 'error', '@next/next/no-head-import-in-document': 'error', '@next/next/no-script-component-in-head': 'error', + '@next/next/no-redirect-in-try-catch': 'error', }, }, 'core-web-vitals': { From c056e39a41a855c7b580ba564172f97279782ed5 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski <piotr.tomczewski@callstack.com> Date: Thu, 29 Aug 2024 23:18:00 +0200 Subject: [PATCH 4/7] test(eslint-plugin-next): update test cases for `no-redirect-in-try-catch` rule --- .../no-redirect-in-try-catch.test.ts | 65 +++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts b/test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts index 61f3982e38620..79e9431a6e1ca 100644 --- a/test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts +++ b/test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts @@ -17,9 +17,42 @@ ruleTester.run('no-redirect-in-try-catch', rule, { `'use server' import { redirect } from "next/navigation" - + + export async function navigate(data) { + redirect(\`/posts/\${data.get('id')}\`) + }`, + `'use server' + + import { redirect, unstable_rethrow } from "next/navigation" + + export async function navigate(data) { + try { + redirect(\`/posts/\${data.get('id')}\`) + } catch (error) { + unstable_rethrow(error) + } + }`, + `'use server' + + import { redirect, unstable_rethrow as rethrow } from "next/navigation" + export async function navigate(data) { + try { redirect(\`/posts/\${data.get('id')}\`) + } catch (error) { + rethrow(error) + } + }`, + `'use server' + + import * as Navigation from "next/navigation" + + export async function navigate(data) { + try { + Navigation.redirect(\`/posts/\${data.get('id')}\`) + } catch (error) { + Navigation.unstable_rethrow(error) + } }`, ], invalid: [ @@ -40,16 +73,16 @@ ruleTester.run('no-redirect-in-try-catch', rule, { errors: [ { message: - 'Do not use `redirect` within a try-catch block. Move the `redirect` call outside of the try-catch block. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', + 'When using `redirect` in a try-catch block, ensure you include `unstable_rethrow` at the start of the catch block to properly handle Next.js errors. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', }, ], }, { code: ` 'use server' - + import { redirect } from "next/navigation" - + export async function navigate(data) { try { if (data.id) { @@ -63,7 +96,29 @@ ruleTester.run('no-redirect-in-try-catch', rule, { errors: [ { message: - 'Do not use `redirect` within a try-catch block. Move the `redirect` call outside of the try-catch block. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', + 'When using `redirect` in a try-catch block, ensure you include `unstable_rethrow` at the start of the catch block to properly handle Next.js errors. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', + }, + ], + }, + { + code: ` + 'use server' + + import { redirect, unstable_rethrow } from "next/navigation" + + export async function navigate(data) { + try { + redirect(\`/posts/\${data.get('id')}\`) + } catch (e) { + console.error(e); + unstable_rethrow(e); + } + }`, + filename: 'app/actions.ts', + errors: [ + { + message: + 'When using `redirect` in a try-catch block, ensure you include `unstable_rethrow` at the start of the catch block to properly handle Next.js errors. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', }, ], }, From 09a132d003e3b10c15039c5b515ee8dfd0eda3b1 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski <piotr.tomczewski@callstack.com> Date: Thu, 29 Aug 2024 23:18:00 +0200 Subject: [PATCH 5/7] feat(eslint-plugin-next): enforce `unstable_rethrow` usage in try-catch blocks --- packages/eslint-plugin-next/package.json | 1 + .../src/rules/no-redirect-in-try-catch.ts | 70 ++++++++++++------- pnpm-lock.yaml | 26 ++++--- 3 files changed, 56 insertions(+), 41 deletions(-) diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 6eaeb64772a58..a3bf09b910dba 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -15,6 +15,7 @@ "fast-glob": "3.3.1" }, "devDependencies": { + "@types/estree": "1.0.5", "eslint": "8.56.0" }, "scripts": { diff --git a/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts b/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts index d07a1a7fb399b..b71c27289eebc 100644 --- a/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts +++ b/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts @@ -1,10 +1,13 @@ import { defineRule } from '../utils/define-rule' +import type { Node, BlockStatement } from 'estree' + const url = 'https://nextjs.org/docs/messages/no-redirect-in-try-catch' export = defineRule({ meta: { docs: { - description: 'Prevent usage of `redirect` in try-catch block.', + description: + 'Ensure that when using `redirect` within a try-catch block, the catch block must start with a call to `unstable_rethrow` to ensures that errors are correctly propagated.', recommended: true, url, }, @@ -12,37 +15,50 @@ export = defineRule({ schema: [], }, create(context) { - function checkForRedirectCall(statement) { - if ( - statement.type === 'ExpressionStatement' && - statement.expression.type === 'CallExpression' && - statement.expression.callee.name === 'redirect' - ) { - context.report({ - node: statement, - message: `Do not use \`redirect\` within a try-catch block. Move the \`redirect\` call outside of the try-catch block. See: ${url}`, - }) - } else if (statement.type === 'BlockStatement') { - statement.body.forEach((innerStatement) => { - checkForRedirectCall(innerStatement) - }) - } else if (statement.type === 'IfStatement') { - checkForRedirectCall(statement.consequent) - if (statement.alternate) { - checkForRedirectCall(statement.alternate) - } + function isRethrowFirstStatement(blockStatement: BlockStatement): boolean { + const firstStatement = blockStatement.body[0] + return ( + firstStatement?.type === 'ExpressionStatement' && + firstStatement.expression.type === 'CallExpression' && + firstStatement.expression.callee.type === 'Identifier' && + firstStatement.expression.callee.name === 'unstable_rethrow' + ) + } + + function containsRedirectCall(node: Node): boolean { + switch (node.type) { + case 'ExpressionStatement': + return ( + node.expression.type === 'CallExpression' && + node.expression.callee.type === 'Identifier' && + node.expression.callee.name === 'redirect' + ) + case 'BlockStatement': + return node.body.some(containsRedirectCall) + case 'IfStatement': + return ( + containsRedirectCall(node.consequent) || + (node.alternate ? containsRedirectCall(node.alternate) : false) + ) + default: + return false } } return { - ImportDeclaration(node) { - if (node.source.value !== 'next/navigation') { - return - } - }, TryStatement(node) { - const tryBlockStatements = node.block.body - tryBlockStatements.forEach(checkForRedirectCall) + const tryBlock = node.block + const catchBlock = node.handler.body + + if ( + containsRedirectCall(tryBlock) && + !isRethrowFirstStatement(catchBlock) + ) { + context.report({ + node: catchBlock, + message: `When using \`redirect\` in a try-catch block, ensure you include \`unstable_rethrow\` at the start of the catch block to properly handle Next.js errors. See: ${url}`, + }) + } }, } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99de34509a773..591dd21595a54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -828,6 +828,9 @@ importers: specifier: 3.3.1 version: 3.3.1 devDependencies: + '@types/estree': + specifier: 1.0.5 + version: 1.0.5 eslint: specifier: 8.56.0 version: 8.56.0 @@ -4977,9 +4980,6 @@ packages: '@types/estree@0.0.39': resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} - '@types/estree@1.0.0': - resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} - '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -19405,7 +19405,7 @@ snapshots: '@types/acorn@4.0.6': dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 '@types/amphtml-validator@1.0.0': dependencies: @@ -19530,12 +19530,10 @@ snapshots: '@types/estree-jsx@1.0.0': dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 '@types/estree@0.0.39': {} - '@types/estree@1.0.0': {} - '@types/estree@1.0.5': {} '@types/events@3.0.0': {} @@ -23280,7 +23278,7 @@ snapshots: estree-util-attach-comments@2.1.1: dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 estree-util-build-jsx@2.2.2: dependencies: @@ -23311,7 +23309,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 esutils@2.0.3: {} @@ -24349,7 +24347,7 @@ snapshots: hast-util-to-estree@2.2.1: dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 '@types/estree-jsx': 1.0.0 '@types/hast': 2.3.1 '@types/unist': 2.0.3 @@ -25045,11 +25043,11 @@ snapshots: is-reference@1.2.1: dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 is-reference@3.0.1: dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 is-regex@1.1.4: dependencies: @@ -26968,7 +26966,7 @@ snapshots: micromark-util-events-to-acorn@1.2.1: dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 estree-util-visit: 1.2.1 micromark-util-types: 1.0.2 uvu: 0.5.6 @@ -28083,7 +28081,7 @@ snapshots: periscopic@3.1.0: dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 estree-walker: 3.0.3 is-reference: 3.0.1 From 91c8508461ecf1f35d118aefba0bd8703bcfab64 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski <piotr.tomczewski@callstack.com> Date: Thu, 29 Aug 2024 23:18:00 +0200 Subject: [PATCH 6/7] feat(eslint-plugin-next): handle import aliases for `redirect` and `unstable_rethrow` --- .../src/rules/no-redirect-in-try-catch.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts b/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts index b71c27289eebc..a51ba4ddf7fe8 100644 --- a/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts +++ b/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts @@ -1,5 +1,5 @@ import { defineRule } from '../utils/define-rule' -import type { Node, BlockStatement } from 'estree' +import type { Node, BlockStatement, ImportDeclaration } from 'estree' const url = 'https://nextjs.org/docs/messages/no-redirect-in-try-catch' @@ -15,13 +15,30 @@ export = defineRule({ schema: [], }, create(context) { + let redirectName = 'redirect' + let rethrowName = 'unstable_rethrow' + + function checkImport(node: ImportDeclaration) { + if (node.source.value === 'next/navigation') { + node.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportSpecifier') { + if (specifier.imported.name === 'redirect') { + redirectName = specifier.local.name + } else if (specifier.imported.name === 'unstable_rethrow') { + rethrowName = specifier.local.name + } + } + }) + } + } + function isRethrowFirstStatement(blockStatement: BlockStatement): boolean { const firstStatement = blockStatement.body[0] return ( firstStatement?.type === 'ExpressionStatement' && firstStatement.expression.type === 'CallExpression' && firstStatement.expression.callee.type === 'Identifier' && - firstStatement.expression.callee.name === 'unstable_rethrow' + firstStatement.expression.callee.name === rethrowName ) } @@ -31,7 +48,7 @@ export = defineRule({ return ( node.expression.type === 'CallExpression' && node.expression.callee.type === 'Identifier' && - node.expression.callee.name === 'redirect' + node.expression.callee.name === redirectName ) case 'BlockStatement': return node.body.some(containsRedirectCall) @@ -46,6 +63,7 @@ export = defineRule({ } return { + ImportDeclaration: checkImport, TryStatement(node) { const tryBlock = node.block const catchBlock = node.handler.body From 6362a6f5ed2987f49ebe5f31f6906d8c076e0e07 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski <piotr.tomczewski@callstack.com> Date: Wed, 4 Sep 2024 11:09:16 +0200 Subject: [PATCH 7/7] feat(eslint-plugin-next): rename `no-redirect-in-try-catch` to `no-redirect-in-try-catch-without-rethrow` and update docs --- .../07-configuring/02-eslint.mdx | 48 +++++++++---------- ...-redirect-in-try-catch-without-rethrow.mdx | 45 +++++++++++++++++ errors/no-redirect-in-try-catch.mdx | 43 ----------------- packages/eslint-plugin-next/src/index.ts | 4 +- ...-redirect-in-try-catch-without-rethrow.ts} | 3 +- ...rect-in-try-catch-without-rethrow.test.ts} | 10 ++-- 6 files changed, 78 insertions(+), 75 deletions(-) create mode 100644 errors/no-redirect-in-try-catch-without-rethrow.mdx delete mode 100644 errors/no-redirect-in-try-catch.mdx rename packages/eslint-plugin-next/src/rules/{no-redirect-in-try-catch.ts => no-redirect-in-try-catch-without-rethrow.ts} (96%) rename test/unit/eslint-plugin-next/{no-redirect-in-try-catch.test.ts => no-redirect-in-try-catch-without-rethrow.test.ts} (92%) diff --git a/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx b/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx index b4ce0448c22e4..bd27123bef013 100644 --- a/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx +++ b/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx @@ -93,30 +93,30 @@ Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/ <Check size={18} /> Enabled in the recommended configuration -| | Rule | Description | -| :-----------------: | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -| <Check size={18} /> | [@next/next/google-font-display](/docs/messages/google-font-display) | Enforce font-display behavior with Google Fonts. | -| <Check size={18} /> | [@next/next/google-font-preconnect](/docs/messages/google-font-preconnect) | Ensure `preconnect` is used with Google Fonts. | -| <Check size={18} /> | [@next/next/inline-script-id](/docs/messages/inline-script-id) | Enforce `id` attribute on `next/script` components with inline content. | -| <Check size={18} /> | [@next/next/next-script-for-ga](/docs/messages/next-script-for-ga) | Prefer `next/script` component when using the inline script for Google Analytics. | -| <Check size={18} /> | [@next/next/no-assign-module-variable](/docs/messages/no-assign-module-variable) | Prevent assignment to the `module` variable. | -| <Check size={18} /> | [@next/next/no-async-client-component](/docs/messages/no-async-client-component) | Prevent client components from being async functions. | -| <Check size={18} /> | [@next/next/no-before-interactive-script-outside-document](/docs/messages/no-before-interactive-script-outside-document) | Prevent usage of `next/script`'s `beforeInteractive` strategy outside of `pages/_document.js`. | -| <Check size={18} /> | [@next/next/no-css-tags](/docs/messages/no-css-tags) | Prevent manual stylesheet tags. | -| <Check size={18} /> | [@next/next/no-document-import-in-page](/docs/messages/no-document-import-in-page) | Prevent importing `next/document` outside of `pages/_document.js`. | -| <Check size={18} /> | [@next/next/no-duplicate-head](/docs/messages/no-duplicate-head) | Prevent duplicate usage of `<Head>` in `pages/_document.js`. | -| <Check size={18} /> | [@next/next/no-head-element](/docs/messages/no-head-element) | Prevent usage of `<head>` element. | -| <Check size={18} /> | [@next/next/no-head-import-in-document](/docs/messages/no-head-import-in-document) | Prevent usage of `next/head` in `pages/_document.js`. | -| <Check size={18} /> | [@next/next/no-html-link-for-pages](/docs/messages/no-html-link-for-pages) | Prevent usage of `<a>` elements to navigate to internal Next.js pages. | -| <Check size={18} /> | [@next/next/no-img-element](/docs/messages/no-img-element) | Prevent usage of `<img>` element due to slower LCP and higher bandwidth. | -| <Check size={18} /> | [@next/next/no-page-custom-font](/docs/messages/no-page-custom-font) | Prevent page-only custom fonts. | -| <Check size={18} /> | [@next/next/no-script-component-in-head](/docs/messages/no-script-component-in-head) | Prevent usage of `next/script` in `next/head` component. | -| <Check size={18} /> | [@next/next/no-styled-jsx-in-document](/docs/messages/no-styled-jsx-in-document) | Prevent usage of `styled-jsx` in `pages/_document.js`. | -| <Check size={18} /> | [@next/next/no-sync-scripts](/docs/messages/no-sync-scripts) | Prevent synchronous scripts. | -| <Check size={18} /> | [@next/next/no-title-in-document-head](/docs/messages/no-title-in-document-head) | Prevent usage of `<title>` with `Head` component from `next/document`. | -| <Check size={18} /> | @next/next/no-typos | Prevent common typos in [Next.js's data fetching functions](/docs/pages/building-your-application/data-fetching) | -| <Check size={18} /> | [@next/next/no-unwanted-polyfillio](/docs/messages/no-unwanted-polyfillio) | Prevent duplicate polyfills from Polyfill.io. | -| <Check size={18} /> | [@next/next/no-redirect-in-try-catch](/docs/messages/no-redirect-in-try-catch) | Prevent usage of `redirect` in try-catch block. | +| | Rule | Description | +| :-----------------: | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| <Check size={18} /> | [@next/next/google-font-display](/docs/messages/google-font-display) | Enforce font-display behavior with Google Fonts. | +| <Check size={18} /> | [@next/next/google-font-preconnect](/docs/messages/google-font-preconnect) | Ensure `preconnect` is used with Google Fonts. | +| <Check size={18} /> | [@next/next/inline-script-id](/docs/messages/inline-script-id) | Enforce `id` attribute on `next/script` components with inline content. | +| <Check size={18} /> | [@next/next/next-script-for-ga](/docs/messages/next-script-for-ga) | Prefer `next/script` component when using the inline script for Google Analytics. | +| <Check size={18} /> | [@next/next/no-assign-module-variable](/docs/messages/no-assign-module-variable) | Prevent assignment to the `module` variable. | +| <Check size={18} /> | [@next/next/no-async-client-component](/docs/messages/no-async-client-component) | Prevent client components from being async functions. | +| <Check size={18} /> | [@next/next/no-before-interactive-script-outside-document](/docs/messages/no-before-interactive-script-outside-document) | Prevent usage of `next/script`'s `beforeInteractive` strategy outside of `pages/_document.js`. | +| <Check size={18} /> | [@next/next/no-css-tags](/docs/messages/no-css-tags) | Prevent manual stylesheet tags. | +| <Check size={18} /> | [@next/next/no-document-import-in-page](/docs/messages/no-document-import-in-page) | Prevent importing `next/document` outside of `pages/_document.js`. | +| <Check size={18} /> | [@next/next/no-duplicate-head](/docs/messages/no-duplicate-head) | Prevent duplicate usage of `<Head>` in `pages/_document.js`. | +| <Check size={18} /> | [@next/next/no-head-element](/docs/messages/no-head-element) | Prevent usage of `<head>` element. | +| <Check size={18} /> | [@next/next/no-head-import-in-document](/docs/messages/no-head-import-in-document) | Prevent usage of `next/head` in `pages/_document.js`. | +| <Check size={18} /> | [@next/next/no-html-link-for-pages](/docs/messages/no-html-link-for-pages) | Prevent usage of `<a>` elements to navigate to internal Next.js pages. | +| <Check size={18} /> | [@next/next/no-img-element](/docs/messages/no-img-element) | Prevent usage of `<img>` element due to slower LCP and higher bandwidth. | +| <Check size={18} /> | [@next/next/no-page-custom-font](/docs/messages/no-page-custom-font) | Prevent page-only custom fonts. | +| <Check size={18} /> | [@next/next/no-script-component-in-head](/docs/messages/no-script-component-in-head) | Prevent usage of `next/script` in `next/head` component. | +| <Check size={18} /> | [@next/next/no-styled-jsx-in-document](/docs/messages/no-styled-jsx-in-document) | Prevent usage of `styled-jsx` in `pages/_document.js`. | +| <Check size={18} /> | [@next/next/no-sync-scripts](/docs/messages/no-sync-scripts) | Prevent synchronous scripts. | +| <Check size={18} /> | [@next/next/no-title-in-document-head](/docs/messages/no-title-in-document-head) | Prevent usage of `<title>` with `Head` component from `next/document`. | +| <Check size={18} /> | @next/next/no-typos | Prevent common typos in [Next.js's data fetching functions](/docs/pages/building-your-application/data-fetching) | +| <Check size={18} /> | [@next/next/no-unwanted-polyfillio](/docs/messages/no-unwanted-polyfillio) | Prevent duplicate polyfills from Polyfill.io. | +| <Check size={18} /> | [@next/next/no-redirect-in-try-catch-without-rethrow](/docs/messages/no-redirect-in-try-catch-without-rethrow) | Ensure that when using `redirect` within a try-catch block, the catch block must start with a call to `unstable_rethrow` to ensure proper error propagation. | If you already have ESLint configured in your application, we recommend extending from this plugin directly instead of including `eslint-config-next` unless a few conditions are met. Refer to the [Recommended Plugin Ruleset](#recommended-plugin-ruleset) to learn more. diff --git a/errors/no-redirect-in-try-catch-without-rethrow.mdx b/errors/no-redirect-in-try-catch-without-rethrow.mdx new file mode 100644 index 0000000000000..7731baf644616 --- /dev/null +++ b/errors/no-redirect-in-try-catch-without-rethrow.mdx @@ -0,0 +1,45 @@ +--- +title: No Redirect in Try-Catch Without Rethrow +--- + +> Ensure that when using `redirect` within a try-catch block, the catch block must start with a call to `unstable_rethrow` to ensure proper error propagation. + +## Why This Error Occurred + +You attempted to use the `redirect` function within a try-catch block without rethrowing the error using `unstable_rethrow`. When `redirect` is called, it throws a `NEXT_REDIRECT` error internally. If this error is caught without being rethrown, it prevents the redirect from executing as intended and suppresses the error handling. + +## Possible Ways to Fix It + +To ensure proper error handling and that the redirect can proceed as intended, the catch block should start with a call to `unstable_rethrow`. This ensures that Next.js's internal error handling is respected and that the redirect is properly executed. + +If you need to handle other errors, you can still do so after the `unstable_rethrow` call. + +## Example + +### Incorrect Usage: + +```javascript +try { + // some code that might throw an error + redirect('/some-path') +} catch (error) { + // handle error +} +``` + +### Correct Usage: + +```javascript +try { + // some code that might throw an error + redirect('/some-path') +} catch (error) { + unstable_rethrow(error) + // handle other errors if necessary +} +``` + +## Useful Links + +- [Next.js Redirect Documentation](/docs/app/building-your-application/routing/redirecting#redirect-function) +- [Next.js Error Handling](/docs/app/building-your-application/routing/error-handling) diff --git a/errors/no-redirect-in-try-catch.mdx b/errors/no-redirect-in-try-catch.mdx deleted file mode 100644 index 1a9174194d109..0000000000000 --- a/errors/no-redirect-in-try-catch.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: No Redirect in Try-Catch ---- - -> Prevent usage of `redirect` in try-catch block. - -## Why This Error Occurred - -You attempted to use the `redirect` function within a try-catch block. When `redirect` is called, it throws a `NEXT_REDIRECT` error internally. If this error is caught by the catch block, it prevents the redirect from executing as intended. - -## Possible Ways to Fix It - -Move the `redirect` call outside of the try-catch block. This ensures that the `NEXT_REDIRECT` error is not caught and the redirect can proceed without interference. - -## Example - -### Incorrect Usage: - -```javascript -try { - // some code that might throw an error - redirect('/some-path') -} catch (error) { - // handle error -} -``` - -### Correct Usage: - -```javascript -try { - // some code that might throw an error -} catch (error) { - // handle error -} -// Redirect outside of the try-catch block -redirect('/some-path') -``` - -## Useful Links - -- [Next.js Redirect Documentation](/docs/app/building-your-application/routing/redirecting#redirect-function) -- [Next.js Error Handling](/docs/app/building-your-application/routing/error-handling) diff --git a/packages/eslint-plugin-next/src/index.ts b/packages/eslint-plugin-next/src/index.ts index 21d6adf966904..babb156900a41 100644 --- a/packages/eslint-plugin-next/src/index.ts +++ b/packages/eslint-plugin-next/src/index.ts @@ -21,7 +21,7 @@ module.exports = { 'no-title-in-document-head': require('./rules/no-title-in-document-head'), 'no-typos': require('./rules/no-typos'), 'no-unwanted-polyfillio': require('./rules/no-unwanted-polyfillio'), - 'no-redirect-in-try-catch': require('./rules/no-redirect-in-try-catch'), + 'no-redirect-in-try-catch-without-rethrow': require('./rules/no-redirect-in-try-catch-without-rethrow'), }, configs: { recommended: { @@ -50,7 +50,7 @@ module.exports = { '@next/next/no-duplicate-head': 'error', '@next/next/no-head-import-in-document': 'error', '@next/next/no-script-component-in-head': 'error', - '@next/next/no-redirect-in-try-catch': 'error', + '@next/next/no-redirect-in-try-catch-without-rethrow': 'error', }, }, 'core-web-vitals': { diff --git a/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts b/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch-without-rethrow.ts similarity index 96% rename from packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts rename to packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch-without-rethrow.ts index a51ba4ddf7fe8..079345a22449a 100644 --- a/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch.ts +++ b/packages/eslint-plugin-next/src/rules/no-redirect-in-try-catch-without-rethrow.ts @@ -1,7 +1,8 @@ import { defineRule } from '../utils/define-rule' import type { Node, BlockStatement, ImportDeclaration } from 'estree' -const url = 'https://nextjs.org/docs/messages/no-redirect-in-try-catch' +const url = + 'https://nextjs.org/docs/messages/no-redirect-in-try-catch-without-rethrow' export = defineRule({ meta: { diff --git a/test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts b/test/unit/eslint-plugin-next/no-redirect-in-try-catch-without-rethrow.test.ts similarity index 92% rename from test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts rename to test/unit/eslint-plugin-next/no-redirect-in-try-catch-without-rethrow.test.ts index 79e9431a6e1ca..22a0aff3de6cb 100644 --- a/test/unit/eslint-plugin-next/no-redirect-in-try-catch.test.ts +++ b/test/unit/eslint-plugin-next/no-redirect-in-try-catch-without-rethrow.test.ts @@ -1,4 +1,4 @@ -import rule from '@next/eslint-plugin-next/dist/rules/no-redirect-in-try-catch' +import rule from '@next/eslint-plugin-next/dist/rules/no-redirect-in-try-catch-without-rethrow' import { RuleTester } from 'eslint' ;(RuleTester as any).setDefaultConfig({ parserOptions: { @@ -12,7 +12,7 @@ import { RuleTester } from 'eslint' }) const ruleTester = new RuleTester() -ruleTester.run('no-redirect-in-try-catch', rule, { +ruleTester.run('no-redirect-in-try-catch-without-rethrow', rule, { valid: [ `'use server' @@ -73,7 +73,7 @@ ruleTester.run('no-redirect-in-try-catch', rule, { errors: [ { message: - 'When using `redirect` in a try-catch block, ensure you include `unstable_rethrow` at the start of the catch block to properly handle Next.js errors. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', + 'When using `redirect` in a try-catch block, ensure you include `unstable_rethrow` at the start of the catch block to properly handle Next.js errors. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch-without-rethrow', }, ], }, @@ -96,7 +96,7 @@ ruleTester.run('no-redirect-in-try-catch', rule, { errors: [ { message: - 'When using `redirect` in a try-catch block, ensure you include `unstable_rethrow` at the start of the catch block to properly handle Next.js errors. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', + 'When using `redirect` in a try-catch block, ensure you include `unstable_rethrow` at the start of the catch block to properly handle Next.js errors. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch-without-rethrow', }, ], }, @@ -118,7 +118,7 @@ ruleTester.run('no-redirect-in-try-catch', rule, { errors: [ { message: - 'When using `redirect` in a try-catch block, ensure you include `unstable_rethrow` at the start of the catch block to properly handle Next.js errors. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch', + 'When using `redirect` in a try-catch block, ensure you include `unstable_rethrow` at the start of the catch block to properly handle Next.js errors. See: https://nextjs.org/docs/messages/no-redirect-in-try-catch-without-rethrow', }, ], },