Skip to content
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 forbidden and unauthorized APIs #72785

Merged
merged 7 commits into from
Nov 21, 2024
Merged

Add forbidden and unauthorized APIs #72785

merged 7 commits into from
Nov 21, 2024

Conversation

huozhi
Copy link
Member

@huozhi huozhi commented Nov 14, 2024

What

Support the forbidden.js and unauthorized.js convention from the app directory and expose two experimental API forbidden() and unauthorized() in the next/navigation. Add restriction that it's only working on canary for now.

Why

Providing these 2 new APIs and error boundaries to fill more missing pieces of the navigation story in Next.js.

Our goal is not trying to mirror the HTTP status codes as APIs, it's providing the semantic actions that act as communication channels between the different parts of the system or the different libraries composability, similar concept to Suspense in React.

The set is limited rather than infinite, we're not going to add different boundaries for each HTTP code. These two are the ones coming up so far which are useful after the application is started and allow you to show the restrictions to the resources you're trying to access. On the application side or library side you just learn the curated sets that framework picked for you.

The distinction is valuable. E.g. unauthorized() would likely take you to a login screen, where as forbidden() might be caught but a child boundary and just indicate that part of the screen isn't available to this user. This also gives library flexibility to just call the abstract API and the upper parent or application side can implement the boundary to handle these errors, instead of deletgating everything to the library to decide what to do.

Example

// next.config.js
module.exports = {
  experimental: {
    authInterrupts: true
  }
}
// app/page.js
import { forbidden } from 'next/navigation'

export default function Page() {
   forbidden()
}

Closes NDX-298

@ijjk
Copy link
Member

ijjk commented Nov 14, 2024

Tests Passed

@ijjk
Copy link
Member

ijjk commented Nov 14, 2024

Stats from current PR

Default Build (Increase detected ⚠️)
General Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
buildDuration 19.3s 16.8s N/A
buildDurationCached 15.4s 15.5s N/A
nodeModulesSize 405 MB 405 MB ⚠️ +207 kB
nextStartRea..uration (ms) 468ms 487ms N/A
Client Bundles (main, webpack) Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
0b69cffb-HASH.js gzip 52.6 kB 52.6 kB N/A
1924.HASH.js gzip 169 B 169 B
195-HASH.js gzip 48.4 kB 48.8 kB ⚠️ +432 B
8589-HASH.js gzip 5.27 kB 5.28 kB N/A
framework-HASH.js gzip 57.4 kB 57.4 kB N/A
main-app-HASH.js gzip 232 B 230 B N/A
main-HASH.js gzip 33.1 kB 33.1 kB N/A
webpack-HASH.js gzip 1.71 kB 1.71 kB N/A
Overall change 48.5 kB 49 kB ⚠️ +432 B
Legacy Client Bundles (polyfills)
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Overall change 39.4 kB 39.4 kB
Client Pages
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
_app-HASH.js gzip 193 B 193 B
_error-HASH.js gzip 192 B 190 B N/A
amp-HASH.js gzip 510 B 510 B
css-HASH.js gzip 341 B 343 B N/A
dynamic-HASH.js gzip 1.84 kB 1.84 kB N/A
edge-ssr-HASH.js gzip 266 B 266 B
head-HASH.js gzip 362 B 364 B N/A
hooks-HASH.js gzip 393 B 392 B N/A
image-HASH.js gzip 4.42 kB 4.42 kB N/A
index-HASH.js gzip 268 B 268 B
link-HASH.js gzip 2.34 kB 2.35 kB N/A
routerDirect..HASH.js gzip 328 B 327 B N/A
script-HASH.js gzip 398 B 396 B N/A
withRouter-HASH.js gzip 325 B 322 B N/A
1afbb74e6ecf..834.css gzip 106 B 106 B
Overall change 1.34 kB 1.34 kB
Client Build Manifests
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
_buildManifest.js gzip 748 B 749 B N/A
Overall change 0 B 0 B
Rendered Page Sizes
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
index.html gzip 522 B 523 B N/A
link.html gzip 537 B 537 B
withRouter.html gzip 519 B 518 B N/A
Overall change 537 B 537 B
Edge SSR bundle Size Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
edge-ssr.js gzip 128 kB 128 kB N/A
page.js gzip 199 kB 200 kB ⚠️ +632 B
Overall change 199 kB 200 kB ⚠️ +632 B
Middleware size
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
middleware-b..fest.js gzip 670 B 668 B N/A
middleware-r..fest.js gzip 156 B 155 B N/A
middleware.js gzip 30.9 kB 31 kB N/A
edge-runtime..pack.js gzip 844 B 844 B
Overall change 844 B 844 B
Next Runtimes Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
732-experime...dev.js gzip 322 B 322 B
732.runtime.dev.js gzip 314 B 314 B
app-page-exp...dev.js gzip 322 kB 323 kB ⚠️ +537 B
app-page-exp..prod.js gzip 125 kB 125 kB ⚠️ +266 B
app-page-tur..prod.js gzip 138 kB 138 kB ⚠️ +267 B
app-page-tur..prod.js gzip 133 kB 133 kB ⚠️ +261 B
app-page.run...dev.js gzip 313 kB 314 kB ⚠️ +541 B
app-page.run..prod.js gzip 121 kB 121 kB ⚠️ +270 B
app-route-ex...dev.js gzip 36 kB 36.1 kB N/A
app-route-ex..prod.js gzip 24.4 kB 24.4 kB N/A
app-route-tu..prod.js gzip 24.4 kB 24.4 kB N/A
app-route-tu..prod.js gzip 24.2 kB 24.2 kB N/A
app-route.ru...dev.js gzip 37.6 kB 37.7 kB N/A
app-route.ru..prod.js gzip 24.2 kB 24.2 kB N/A
pages-api-tu..prod.js gzip 9.57 kB 9.57 kB
pages-api.ru...dev.js gzip 11.4 kB 11.4 kB
pages-api.ru..prod.js gzip 9.56 kB 9.56 kB
pages-turbo...prod.js gzip 21 kB 21 kB
pages.runtim...dev.js gzip 26.6 kB 26.6 kB
pages.runtim..prod.js gzip 21 kB 21 kB
server.runti..prod.js gzip 916 kB 916 kB N/A
Overall change 1.25 MB 1.25 MB ⚠️ +2.14 kB
build cache Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-13-pick_up_conventions Change
0.pack gzip 2.01 MB 2.03 MB ⚠️ +14.8 kB
index.pack gzip 147 kB 148 kB ⚠️ +527 B
Overall change 2.16 MB 2.17 MB ⚠️ +15.3 kB
Diff details
Diff for page.js

Diff too large to display

Diff for middleware.js

Diff too large to display

Diff for edge-ssr.js

Diff too large to display

Diff for image-HASH.js
@@ -1,7 +1,7 @@
 (self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
   [2983],
   {
-    /***/ 6745: /***/ (
+    /***/ 7391: /***/ (
       __unused_webpack_module,
       __unused_webpack_exports,
       __webpack_require__
@@ -9,7 +9,7 @@
       (window.__NEXT_P = window.__NEXT_P || []).push([
         "/image",
         function () {
-          return __webpack_require__(5675);
+          return __webpack_require__(1489);
         },
       ]);
       if (false) {
@@ -18,7 +18,7 @@
       /***/
     },
 
-    /***/ 9053: /***/ (module, exports, __webpack_require__) => {
+    /***/ 9313: /***/ (module, exports, __webpack_require__) => {
       "use strict";
       /* __next_internal_client_entry_do_not_use__  cjs */
       Object.defineProperty(exports, "__esModule", {
@@ -40,17 +40,17 @@
         __webpack_require__(6093)
       );
       const _head = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(8808)
+        __webpack_require__(7964)
       );
-      const _getimgprops = __webpack_require__(1945);
-      const _imageconfig = __webpack_require__(7668);
-      const _imageconfigcontextsharedruntime = __webpack_require__(1694);
-      const _warnonce = __webpack_require__(1876);
-      const _routercontextsharedruntime = __webpack_require__(5575);
+      const _getimgprops = __webpack_require__(8821);
+      const _imageconfig = __webpack_require__(664);
+      const _imageconfigcontextsharedruntime = __webpack_require__(6418);
+      const _warnonce = __webpack_require__(7360);
+      const _routercontextsharedruntime = __webpack_require__(4203);
       const _imageloader = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(3589)
+        __webpack_require__(2489)
       );
-      const _usemergedref = __webpack_require__(6746);
+      const _usemergedref = __webpack_require__(2454);
       // This is replaced by webpack define plugin
       const configEnv = {
         deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
@@ -371,7 +371,7 @@
       /***/
     },
 
-    /***/ 6746: /***/ (module, exports, __webpack_require__) => {
+    /***/ 2454: /***/ (module, exports, __webpack_require__) => {
       "use strict";
 
       Object.defineProperty(exports, "__esModule", {
@@ -432,7 +432,7 @@
       /***/
     },
 
-    /***/ 1945: /***/ (
+    /***/ 8821: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -448,9 +448,9 @@
           return getImgProps;
         },
       });
-      const _warnonce = __webpack_require__(1876);
-      const _imageblursvg = __webpack_require__(6704);
-      const _imageconfig = __webpack_require__(7668);
+      const _warnonce = __webpack_require__(7360);
+      const _imageblursvg = __webpack_require__(5884);
+      const _imageconfig = __webpack_require__(664);
       const VALID_LOADING_VALUES =
         /* unused pure expression or super */ null && [
           "lazy",
@@ -824,7 +824,7 @@
       /***/
     },
 
-    /***/ 6704: /***/ (__unused_webpack_module, exports) => {
+    /***/ 5884: /***/ (__unused_webpack_module, exports) => {
       "use strict";
       /**
        * A shared function, used on both client and server, to generate a SVG blur placeholder.
@@ -879,7 +879,7 @@
       /***/
     },
 
-    /***/ 965: /***/ (
+    /***/ 9345: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -906,10 +906,10 @@
         },
       });
       const _interop_require_default = __webpack_require__(1739);
-      const _getimgprops = __webpack_require__(1945);
-      const _imagecomponent = __webpack_require__(9053);
+      const _getimgprops = __webpack_require__(8821);
+      const _imagecomponent = __webpack_require__(9313);
       const _imageloader = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(3589)
+        __webpack_require__(2489)
       );
       function getImageProps(imgProps) {
         const { props } = (0, _getimgprops.getImgProps)(imgProps, {
@@ -941,7 +941,7 @@
       /***/
     },
 
-    /***/ 3589: /***/ (__unused_webpack_module, exports) => {
+    /***/ 2489: /***/ (__unused_webpack_module, exports) => {
       "use strict";
 
       Object.defineProperty(exports, "__esModule", {
@@ -976,7 +976,7 @@
       /***/
     },
 
-    /***/ 5675: /***/ (
+    /***/ 1489: /***/ (
       __unused_webpack_module,
       __webpack_exports__,
       __webpack_require__
@@ -993,8 +993,8 @@
 
       // EXTERNAL MODULE: ./node_modules/.pnpm/react@19.0.0-rc-380f5d67-20241113/node_modules/react/jsx-runtime.js
       var jsx_runtime = __webpack_require__(6322);
-      // EXTERNAL MODULE: ./node_modules/.pnpm/next@file+..+main-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-380f5d67-20241113_re_d7fg766ptstyt4prarg74ol27i/node_modules/next/image.js
-      var next_image = __webpack_require__(1695);
+      // EXTERNAL MODULE: ./node_modules/.pnpm/next@file+..+diff-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-380f5d67-20241113_re_k6jswiqskvoeqe45yhuljotqne/node_modules/next/image.js
+      var next_image = __webpack_require__(8106);
       var image_default = /*#__PURE__*/ __webpack_require__.n(next_image); // ./pages/nextjs.png
       /* harmony default export */ const nextjs = {
         src: "/_next/static/media/nextjs.cae0b805.png",
@@ -1024,12 +1024,12 @@
       /***/
     },
 
-    /***/ 1695: /***/ (
+    /***/ 8106: /***/ (
       module,
       __unused_webpack_exports,
       __webpack_require__
     ) => {
-      module.exports = __webpack_require__(965);
+      module.exports = __webpack_require__(9345);
 
       /***/
     },
@@ -1039,7 +1039,7 @@
     /******/ var __webpack_exec__ = (moduleId) =>
       __webpack_require__((__webpack_require__.s = moduleId));
     /******/ __webpack_require__.O(0, [636, 6593, 8792], () =>
-      __webpack_exec__(6745)
+      __webpack_exec__(7391)
     );
     /******/ var __webpack_exports__ = __webpack_require__.O();
     /******/ _N_E = __webpack_exports__;
Diff for 195-HASH.js

Diff too large to display

Diff for app-page-exp..ntime.dev.js
failed to diff
Diff for app-page-exp..time.prod.js

Diff too large to display

Diff for app-page-tur..time.prod.js

Diff too large to display

Diff for app-page-tur..time.prod.js

Diff too large to display

Diff for app-page.runtime.dev.js

Diff too large to display

Diff for app-page.runtime.prod.js

Diff too large to display

Diff for app-route-ex..ntime.dev.js

Diff too large to display

Diff for app-route-ex..time.prod.js

Diff too large to display

Diff for app-route-tu..time.prod.js

Diff too large to display

Diff for app-route-tu..time.prod.js

Diff too large to display

Diff for app-route.runtime.dev.js

Diff too large to display

Diff for app-route.ru..time.prod.js

Diff too large to display

Diff for server.runtime.prod.js

Diff too large to display

Commit: 8c05d12

packages/next/src/client/components/forbidden-error.tsx Outdated Show resolved Hide resolved
packages/next/src/client/components/forbidden.ts Outdated Show resolved Hide resolved
packages/next/src/client/components/unauthorized-error.tsx Outdated Show resolved Hide resolved
packages/next/src/client/components/unauthorized.ts Outdated Show resolved Hide resolved
test/e2e/app-dir/forbidden/basic/basic.test.ts Outdated Show resolved Hide resolved
test/e2e/app-dir/unauthorized/basic/basic.test.ts Outdated Show resolved Hide resolved
@simoncave
Copy link

I'm very happy this is happening. Two notes:

  1. The unauthorized() function and unauthorized.js file will work really well for simple authentication schemes. If my component sends a request to an API that returns a 401 error, I can call unauthorized(), then I can use a global unauthorized.js boundary file to redirect the user to the login page, or just render the login page directly.

    But for more complex authentication schemes, the 401 response could mean one of two things:

    1. Your authentication token is missing or expired — you need to authenticate.
    2. Your authentication token does not have the required assurance or scopes for this particular request — you need to step-up your authentication.1

    In cases where we want to step-up authentication (e.g. by using a stronger form of authentication, like a security key, or approval from a trusted device, or simply reauthenticating for a particular transaction), I would want to be able provide parameters to unauthorized() that are available in unauthorized.js. Ideally, this would be any structured data that can be serialized by React, but even just a single string would suffice.

    Theoretically, there might be cases where you would want to have the same ability to send data along with forbidden() and notFound() too.

  2. With unauthorized(), forbidden(), and notFound(), we have abstractions over HTTP 401, 403, and 404 errors respectively. It begs the question, are there more 4xx statuses that might warrant a similar treatment? Would it be useful to be able to call gone() to trigger a 410 response that I can catch in gone.js2? And should there be a general abstraction over all 4xx HTTP statuses?

    If the answer to that last question is yes, then I have a proposal...

    The next/navigation package would export a reject function. For the 3xx HTTP status class, you use redirect/permanentRedirect. For the 4xx HTTP status class, you use reject. It can be called with no arguments, which results in a 400 status. But typically, you will call it with a status code (e.g. reject(401), reject(403), reject(404)).

    The rejected.js boundary file can be added to catch rejection errors (but more specific boundaries like not-found.js take precedence). It receives the status as a prop.

    Per my first note, it could support an optional detail (or cause?) argument as the second parameter (support for serializable structured data would be great, but a string, at least initially, would suffice).

Footnotes

  1. Instinctively, this might seem like a 403 scenario, but the devil is in the details. 403 means "I know you you are, and you're not allowed to do that". 401 typically means "I don't know who you are, so I can't let you do that", but it can also mean "I don't have enough assurance that you are who you say you are to do that".

  2. And logically, should I be able to call imATeapot() to trigger a 418 response that I can catch in im-a-teapot.js?

@huozhi huozhi force-pushed the 11-13-pick_up_conventions branch from d247e63 to e5ec2de Compare November 15, 2024 23:13
@ijjk ijjk added create-next-app Related to our CLI tool for quickly starting a new Next.js application. examples Issue was opened via the examples template. labels Nov 15, 2024
@huozhi huozhi force-pushed the 11-13-error_detector branch from 4f76a06 to 80a143c Compare November 15, 2024 23:27
Base automatically changed from 11-13-error_detector to canary November 18, 2024 10:24
@huozhi huozhi force-pushed the 11-13-pick_up_conventions branch from f6caf88 to 35e1233 Compare November 18, 2024 10:36
@huozhi
Copy link
Member Author

huozhi commented Nov 18, 2024

@simoncave I can answer the 2n question now. Our goal is not trying to mirror the HTTP status codes as APIs, it's providing the semantic actions that act as communication channels between the different parts of the system or the different libraries composability, similar concept to Suspense in React.

The set is limited rather than infinite, we're not going to add different boundaries for each HTTP code. These two are the ones coming up so far which are useful after the application is started and allow you to show the restrictions to the resources you're trying to access. On the application side or library side you just learn the curated sets that framework picked for you.

The distinction is valuable. E.g. unauthorized() would likely take you to a login screen, where as forbidden() might be caught but a child boundary and just indicate that part of the screen isn't available to this user. This also gives library flexibility to just call the abstract API and the upper parent or application side can implement the boundary to handle these errors, instead of deletgating everything to the library to decide what to do.

@huozhi huozhi changed the title pick up the convention in structure Add forbidden and unauthorized APIs Nov 19, 2024
@huozhi huozhi force-pushed the 11-13-pick_up_conventions branch from 917b0bf to 91c6b0d Compare November 19, 2024 15:09
@huozhi huozhi marked this pull request as ready for review November 19, 2024 17:23
@huozhi huozhi requested review from ztanner and gnoff November 19, 2024 17:23
@huozhi huozhi force-pushed the 11-13-pick_up_conventions branch 2 times, most recently from 388f020 to f8437f1 Compare November 20, 2024 22:33
@huozhi huozhi force-pushed the 11-13-pick_up_conventions branch 2 times, most recently from cab2e9d to 671a3f4 Compare November 21, 2024 15:36
fix test fixture

improve test

handle no matched boundary case

test: reorganize the default test

update metadata error convention (#72834)

add element validation and remove unused boundaries

use flag

pass down flag everywhere
@huozhi huozhi force-pushed the 11-13-pick_up_conventions branch from 671a3f4 to 64c3d67 Compare November 21, 2024 16:13
packages/next/src/server/dev/static-paths-worker.ts Outdated Show resolved Hide resolved
packages/next/src/client/components/forbidden.ts Outdated Show resolved Hide resolved
@huozhi huozhi requested a review from ztanner November 21, 2024 18:59
@huozhi huozhi merged commit 37f26ac into canary Nov 21, 2024
108 of 113 checks passed
@huozhi huozhi deleted the 11-13-pick_up_conventions branch November 21, 2024 19:59
wyattjoh pushed a commit that referenced this pull request Nov 28, 2024
@github-actions github-actions bot added the locked label Dec 6, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Dec 6, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
create-next-app Related to our CLI tool for quickly starting a new Next.js application. created-by: Next.js team PRs by the Next.js team. examples Issue was opened via the examples template. locked tests Turbopack Related to Turbopack with Next.js. type: next
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants