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

Introduce experimental Request Interceptors #70961

Draft
wants to merge 34 commits into
base: canary
Choose a base branch
from
Draft

Conversation

unstubbable
Copy link
Contributor

@unstubbable unstubbable commented Oct 8, 2024

Note

This API is unstable and might change or not be shipped as stable.

This PR introduces Request Interceptors as a complementary solution to Middleware. They allow users to run code at the origin – in the same Function/process as the page, server action, or route handler – before the page is rendered or the server action or route handler is executed.

Goals

  • Improve DX by providing a mechanism to run code at the origin before a page is rendered, a server action is executed, or a route handler is invoked, with full access to Node.js APIs.
  • Execute code after the app shell is served to prevent delays in rendering and improve metrics like First Contentful Paint (FCP).
  • Integrate with the app router file conventions instead of using a matcher config.

Background

Next.js Middleware allows running code before a request is processed but has limitations:

  • Limited Node.js API Support: Middleware runs in the Edge Runtime, which doesn't support certain Node.js modules like fs, net, child_process, crypto, and others.
  • Performance Overhead: Middleware executes before serving cached content, potentially delaying the response and negatively impacting metrics like First Contentful Paint (FCP).
  • Separate Process Execution: Middleware runs in a separate process without direct access to the environment of the main application.
  • Composability: Only a single global Middleware can be defined. A matcher config must be used to target specific routes, and discriminating logic for different routes must be handled programmatically, which can lead to complex and less maintainable code.

Proposal

To address these challenges, we propose introducing Request Interceptors – a complementary solution to Middleware that runs code at the origin, in the same process as the page, server action, or route handler.

Interceptors are defined in interceptor.ts files, which can be placed anywhere in the app directory. They can be nested and aligned with your routing file structure.

They run in the same environment as the page, route handler, or server action that the request is intended for.

To enable Interceptors, set experimental.interceptors to true in your next.config.ts file.

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  experimental: { interceptors: true },
};

export default nextConfig;

An Interceptor exports a default asynchronous function that receives a NextRequest object:

export default async function intercept(request: NextRequest): Promise<void> {
  // Interceptor logic...

  // Control flow options:
  redirect('/login');
  // or
  notFound();
  // or
  notAuthorized(); // coming soon!
  // or
  throw new Error('Custom error message');
}

A page is only rendered when an interceptor at the same route segment, and all interceptors above it, are resolved without throwing or redirecting. The same applies for the execution of route handlers and server actions.

For parallel routes, Interceptors at the same segment level are executed concurrently.

Thrown errors are handled by the nearest error boundary.

Example Usage: Authentication

If you want to protect a whole subtree of your app directory – for example, everything at /dashboard and below – using an authentication provider backed by a regional database, an Interceptor might be a good fit to guard these routes.

In this scenario, create app/dashboard/interceptor.ts with the following contents:

import { auth } from '@/auth';
import { redirect } from 'next/navigation';

const signInPathname = '/dashboard/sign-in';

export default async function intercept(request: NextRequest): Promise<void> {
  // This will also seed React's cache, so that the session is already
  // available when the `auth` function is called in server components.
  const session = await auth();

  if (!session && request.nextUrl.pathname !== signInPathname) {
    redirect(signInPathname);
  }
}

With lib/auth.ts looking something like this:

import { cache } from 'react';

export const auth = cache(async () => {
  // read session cookie from `cookies()`
  // use session cookie to read user from database
})

This assumes that there’s also app/dashboard/layout.tsx and app/dashboard/loading.tsx (containing a skeleton of the dashboard UI, for example).

Note that we’ve located the sign-in page inside app/dashboard. You don’t have to do this, but it avoids layout thrashing and UI flashes when redirecting logged-out users from the dashboard shell to the sign-in page, because both pages then share a layout.

Optionally, to further mitigate UI flashes for logged-out users, Middleware may be used in conjunction with such an authentication Interceptor. In the middleware function, the existence of a session cookie can be checked early to allow redirecting users without a cookie before rendering has started:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const signInPathname = '/dashboard/sign-in';

export function middleware(request: NextRequest): NextResponse {
  if (
    !request.nextUrl.pathname.startsWith(signInPathname) &&
    !request.cookies.has('session')
  ) {
    return NextResponse.redirect(new URL(signInPathname, request.url));
  }

  return NextResponse.next();
}

export const config = { matcher: ['/dashboard/:path*'] };

In this case, the Middleware redirect occurs at the Edge, with the appropriate HTTP status code (e.g., 302), ensuring that users without a session cookie are redirected before any content is rendered.

A common issue with only using Middleware for authentication is that database reads can be too slow, resulting in poor loading performance, as it blocks the response. This is why we recommend combining a lightweight check in Middleware with proper authentication during rendering.

Caveats

By design, since Interceptors don't prevent the initial shell from being served to users, they cannot change the response status code or headers. Setting cookies needs to be done either in Middleware, server actions or route handlers instead.

Interceptors will opt pages into dynamic rendering. Therefore, they're best suited to intercept personalized routes, as they will prevent a route from being fully static.

Interceptors are executed sequentially, and they delay the rendering of a page or the execution of a server action or route handler. Interceptors should therefore be efficient to avoid unnecessary delays and impacting response times.

Support for Edge Runtime will be added in a follow-up PR.

Alternatives

  • If the limitations of the Edge Runtime and performance impacts are not an issue, you can continue using Middleware for request handling. Middleware can also be used in conjunction with Request Interceptors.
  • Implement authentication or other logic directly within server components, route handlers, or server actions.

@ijjk ijjk added created-by: Next.js team PRs by the Next.js team. tests Turbopack Related to Turbopack with Next.js. type: next labels Oct 8, 2024
@ijjk
Copy link
Member

ijjk commented Oct 8, 2024

Tests Passed

@ijjk
Copy link
Member

ijjk commented Oct 8, 2024

Stats from current PR

Default Build (Increase detected ⚠️)
General Overall increase ⚠️
vercel/next.js canary vercel/next.js interceptors Change
buildDuration 18.8s 17.1s N/A
buildDurationCached 16.2s 13.9s N/A
nodeModulesSize 370 MB 371 MB ⚠️ +863 kB
nextStartRea..uration (ms) 449ms 427ms N/A
Client Bundles (main, webpack)
vercel/next.js canary vercel/next.js interceptors Change
1526.HASH.js gzip 170 B 169 B N/A
1698-HASH.js gzip 5.27 kB 5.27 kB N/A
3463-HASH.js gzip 43.4 kB 43.5 kB N/A
d1e65033-HASH.js gzip 52.8 kB 52.8 kB N/A
framework-HASH.js gzip 57.5 kB 57.5 kB N/A
main-app-HASH.js gzip 233 B 233 B
main-HASH.js gzip 32.7 kB 32.7 kB N/A
webpack-HASH.js gzip 1.71 kB 1.71 kB
Overall change 1.94 kB 1.94 kB
Legacy Client Bundles (polyfills)
vercel/next.js canary vercel/next.js interceptors 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 interceptors Change
_app-HASH.js gzip 193 B 193 B
_error-HASH.js gzip 192 B 192 B
amp-HASH.js gzip 511 B 512 B N/A
css-HASH.js gzip 343 B 341 B N/A
dynamic-HASH.js gzip 1.84 kB 1.85 kB N/A
edge-ssr-HASH.js gzip 266 B 266 B
head-HASH.js gzip 364 B 363 B N/A
hooks-HASH.js gzip 392 B 389 B N/A
image-HASH.js gzip 4.41 kB 4.41 kB N/A
index-HASH.js gzip 268 B 268 B
link-HASH.js gzip 2.78 kB 2.78 kB N/A
routerDirect..HASH.js gzip 329 B 328 B N/A
script-HASH.js gzip 396 B 396 B
withRouter-HASH.js gzip 325 B 324 B N/A
1afbb74e6ecf..834.css gzip 106 B 106 B
Overall change 1.42 kB 1.42 kB
Client Build Manifests
vercel/next.js canary vercel/next.js interceptors Change
_buildManifest.js gzip 747 B 750 B N/A
Overall change 0 B 0 B
Rendered Page Sizes
vercel/next.js canary vercel/next.js interceptors Change
index.html gzip 525 B 525 B
link.html gzip 539 B 539 B
withRouter.html gzip 519 B 521 B N/A
Overall change 1.06 kB 1.06 kB
Edge SSR bundle Size Overall increase ⚠️
vercel/next.js canary vercel/next.js interceptors Change
edge-ssr.js gzip 129 kB 129 kB ⚠️ +155 B
page.js gzip 187 kB 188 kB ⚠️ +1.45 kB
Overall change 316 kB 317 kB ⚠️ +1.6 kB
Middleware size Overall increase ⚠️
vercel/next.js canary vercel/next.js interceptors Change
middleware-b..fest.js gzip 668 B 669 B N/A
middleware-r..fest.js gzip 155 B 156 B N/A
middleware.js gzip 30.3 kB 30.8 kB ⚠️ +493 B
edge-runtime..pack.js gzip 844 B 844 B
Overall change 31.1 kB 31.6 kB ⚠️ +493 B
Next Runtimes Overall increase ⚠️
vercel/next.js canary vercel/next.js interceptors Change
973-experime...dev.js gzip 322 B 322 B
973.runtime.dev.js gzip 314 B 314 B
app-page-exp...dev.js gzip 310 kB 312 kB ⚠️ +1.86 kB
app-page-exp..prod.js gzip 119 kB 121 kB ⚠️ +1.5 kB
app-page-tur..prod.js gzip 133 kB 134 kB ⚠️ +1.53 kB
app-page-tur..prod.js gzip 128 kB 129 kB ⚠️ +1.49 kB
app-page.run...dev.js gzip 300 kB 302 kB ⚠️ +1.86 kB
app-page.run..prod.js gzip 115 kB 116 kB ⚠️ +1.51 kB
app-route-ex...dev.js gzip 34.4 kB 36.1 kB ⚠️ +1.69 kB
app-route-ex..prod.js gzip 23.3 kB 24.6 kB ⚠️ +1.31 kB
app-route-tu..prod.js gzip 23.3 kB 24.6 kB ⚠️ +1.31 kB
app-route-tu..prod.js gzip 23.1 kB 24.5 kB ⚠️ +1.31 kB
app-route.ru...dev.js gzip 36 kB 37.7 kB ⚠️ +1.68 kB
app-route.ru..prod.js gzip 23.1 kB 24.5 kB ⚠️ +1.31 kB
pages-api-tu..prod.js gzip 9.6 kB 9.6 kB
pages-api.ru...dev.js gzip 11.4 kB 11.4 kB
pages-api.ru..prod.js gzip 9.6 kB 9.6 kB
pages-turbo...prod.js gzip 20.9 kB 21.6 kB ⚠️ +670 B
pages.runtim...dev.js gzip 26.5 kB 27.4 kB ⚠️ +922 B
pages.runtim..prod.js gzip 20.9 kB 21.6 kB ⚠️ +671 B
server.runti..prod.js gzip 59.1 kB 59.3 kB ⚠️ +147 B
Overall change 1.43 MB 1.45 MB ⚠️ +20.8 kB
build cache Overall increase ⚠️
vercel/next.js canary vercel/next.js interceptors Change
0.pack gzip 1.84 MB 1.84 MB ⚠️ +1.97 kB
index.pack gzip 143 kB 143 kB N/A
Overall change 1.84 MB 1.84 MB ⚠️ +1.97 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([
   [8358],
   {
-    /***/ 4519: /***/ (
+    /***/ 766: /***/ (
       __unused_webpack_module,
       __unused_webpack_exports,
       __webpack_require__
@@ -9,7 +9,7 @@
       (window.__NEXT_P = window.__NEXT_P || []).push([
         "/image",
         function () {
-          return __webpack_require__(6366);
+          return __webpack_require__(8898);
         },
       ]);
       if (false) {
@@ -18,7 +18,7 @@
       /***/
     },
 
-    /***/ 7847: /***/ (module, exports, __webpack_require__) => {
+    /***/ 8501: /***/ (module, exports, __webpack_require__) => {
       "use strict";
       /* __next_internal_client_entry_do_not_use__  cjs */
       Object.defineProperty(exports, "__esModule", {
@@ -40,17 +40,17 @@
         __webpack_require__(133)
       );
       const _head = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(6142)
+        __webpack_require__(294)
       );
-      const _getimgprops = __webpack_require__(2989);
-      const _imageconfig = __webpack_require__(2948);
-      const _imageconfigcontextsharedruntime = __webpack_require__(3394);
-      const _warnonce = __webpack_require__(6308);
-      const _routercontextsharedruntime = __webpack_require__(932);
+      const _getimgprops = __webpack_require__(2367);
+      const _imageconfig = __webpack_require__(9037);
+      const _imageconfigcontextsharedruntime = __webpack_require__(6876);
+      const _warnonce = __webpack_require__(5603);
+      const _routercontextsharedruntime = __webpack_require__(6967);
       const _imageloader = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(4394)
+        __webpack_require__(5093)
       );
-      const _usemergedref = __webpack_require__(9673);
+      const _usemergedref = __webpack_require__(9386);
       // This is replaced by webpack define plugin
       const configEnv = {
         deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
@@ -371,7 +371,7 @@
       /***/
     },
 
-    /***/ 9673: /***/ (module, exports, __webpack_require__) => {
+    /***/ 9386: /***/ (module, exports, __webpack_require__) => {
       "use strict";
 
       Object.defineProperty(exports, "__esModule", {
@@ -432,7 +432,7 @@
       /***/
     },
 
-    /***/ 2989: /***/ (
+    /***/ 2367: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -448,9 +448,9 @@
           return getImgProps;
         },
       });
-      const _warnonce = __webpack_require__(6308);
-      const _imageblursvg = __webpack_require__(9492);
-      const _imageconfig = __webpack_require__(2948);
+      const _warnonce = __webpack_require__(5603);
+      const _imageblursvg = __webpack_require__(2052);
+      const _imageconfig = __webpack_require__(9037);
       const VALID_LOADING_VALUES =
         /* unused pure expression or super */ null && [
           "lazy",
@@ -823,7 +823,7 @@
       /***/
     },
 
-    /***/ 9492: /***/ (__unused_webpack_module, exports) => {
+    /***/ 2052: /***/ (__unused_webpack_module, exports) => {
       "use strict";
       /**
        * A shared function, used on both client and server, to generate a SVG blur placeholder.
@@ -878,7 +878,7 @@
       /***/
     },
 
-    /***/ 9256: /***/ (
+    /***/ 3038: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -905,10 +905,10 @@
         },
       });
       const _interop_require_default = __webpack_require__(9608);
-      const _getimgprops = __webpack_require__(2989);
-      const _imagecomponent = __webpack_require__(7847);
+      const _getimgprops = __webpack_require__(2367);
+      const _imagecomponent = __webpack_require__(8501);
       const _imageloader = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(4394)
+        __webpack_require__(5093)
       );
       function getImageProps(imgProps) {
         const { props } = (0, _getimgprops.getImgProps)(imgProps, {
@@ -940,7 +940,7 @@
       /***/
     },
 
-    /***/ 4394: /***/ (__unused_webpack_module, exports) => {
+    /***/ 5093: /***/ (__unused_webpack_module, exports) => {
       "use strict";
 
       Object.defineProperty(exports, "__esModule", {
@@ -975,7 +975,7 @@
       /***/
     },
 
-    /***/ 6366: /***/ (
+    /***/ 8898: /***/ (
       __unused_webpack_module,
       __webpack_exports__,
       __webpack_require__
@@ -992,8 +992,8 @@
 
       // EXTERNAL MODULE: ./node_modules/.pnpm/react@19.0.0-rc-2d16326d-20240930/node_modules/react/jsx-runtime.js
       var jsx_runtime = __webpack_require__(9837);
-      // EXTERNAL MODULE: ./node_modules/.pnpm/next@file+..+main-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-2d16326d-20240930_re_aw34735d3a4s5ybwraoeym2z3m/node_modules/next/image.js
-      var next_image = __webpack_require__(6020);
+      // EXTERNAL MODULE: ./node_modules/.pnpm/next@file+..+diff-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-2d16326d-20240930_re_twsodcu6u2xc4vttzu7xhu5gra/node_modules/next/image.js
+      var next_image = __webpack_require__(3843);
       var image_default = /*#__PURE__*/ __webpack_require__.n(next_image); // CONCATENATED MODULE: ./pages/nextjs.png
       /* harmony default export */ const nextjs = {
         src: "/_next/static/media/nextjs.cae0b805.png",
@@ -1023,12 +1023,12 @@
       /***/
     },
 
-    /***/ 6020: /***/ (
+    /***/ 3843: /***/ (
       module,
       __unused_webpack_exports,
       __webpack_require__
     ) => {
-      module.exports = __webpack_require__(9256);
+      module.exports = __webpack_require__(3038);
 
       /***/
     },
@@ -1038,7 +1038,7 @@
     /******/ var __webpack_exec__ = (moduleId) =>
       __webpack_require__((__webpack_require__.s = moduleId));
     /******/ __webpack_require__.O(0, [2888, 9774, 179], () =>
-      __webpack_exec__(4519)
+      __webpack_exec__(766)
     );
     /******/ var __webpack_exports__ = __webpack_require__.O();
     /******/ _N_E = __webpack_exports__;
Diff for 1698-HASH.js
@@ -1,8 +1,8 @@
 "use strict";
 (self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
-  [1698],
+  [2859],
   {
-    /***/ 1698: /***/ (module, exports, __webpack_require__) => {
+    /***/ 2859: /***/ (module, exports, __webpack_require__) => {
       /* __next_internal_client_entry_do_not_use__  cjs */
       Object.defineProperty(exports, "__esModule", {
         value: true,
@@ -13,27 +13,27 @@
           return Image;
         },
       });
-      const _interop_require_default = __webpack_require__(3280);
-      const _interop_require_wildcard = __webpack_require__(8464);
-      const _jsxruntime = __webpack_require__(673);
+      const _interop_require_default = __webpack_require__(9218);
+      const _interop_require_wildcard = __webpack_require__(8553);
+      const _jsxruntime = __webpack_require__(9348);
       const _react = /*#__PURE__*/ _interop_require_wildcard._(
-        __webpack_require__(254)
+        __webpack_require__(8196)
       );
       const _reactdom = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(177)
+        __webpack_require__(8174)
       );
       const _head = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(4591)
+        __webpack_require__(3039)
       );
-      const _getimgprops = __webpack_require__(6509);
-      const _imageconfig = __webpack_require__(1545);
-      const _imageconfigcontextsharedruntime = __webpack_require__(9041);
-      const _warnonce = __webpack_require__(7147);
-      const _routercontextsharedruntime = __webpack_require__(7112);
+      const _getimgprops = __webpack_require__(4645);
+      const _imageconfig = __webpack_require__(8661);
+      const _imageconfigcontextsharedruntime = __webpack_require__(5611);
+      const _warnonce = __webpack_require__(3975);
+      const _routercontextsharedruntime = __webpack_require__(4332);
       const _imageloader = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(4980)
+        __webpack_require__(9206)
       );
-      const _usemergedref = __webpack_require__(3096);
+      const _usemergedref = __webpack_require__(900);
       // This is replaced by webpack define plugin
       const configEnv = {
         deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
@@ -355,7 +355,7 @@
       /***/
     },
 
-    /***/ 3096: /***/ (module, exports, __webpack_require__) => {
+    /***/ 900: /***/ (module, exports, __webpack_require__) => {
       Object.defineProperty(exports, "__esModule", {
         value: true,
       });
@@ -365,7 +365,7 @@
           return useMergedRef;
         },
       });
-      const _react = __webpack_require__(254);
+      const _react = __webpack_require__(8196);
       function useMergedRef(refA, refB) {
         const cleanupA = (0, _react.useRef)(() => {});
         const cleanupB = (0, _react.useRef)(() => {});
@@ -414,7 +414,7 @@
       /***/
     },
 
-    /***/ 5225: /***/ (
+    /***/ 4551: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -428,9 +428,9 @@
           return AmpStateContext;
         },
       });
-      const _interop_require_default = __webpack_require__(3280);
+      const _interop_require_default = __webpack_require__(9218);
       const _react = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(254)
+        __webpack_require__(8196)
       );
       const AmpStateContext = _react.default.createContext({});
       if (false) {
@@ -439,7 +439,7 @@
       /***/
     },
 
-    /***/ 4457: /***/ (__unused_webpack_module, exports) => {
+    /***/ 7094: /***/ (__unused_webpack_module, exports) => {
       Object.defineProperty(exports, "__esModule", {
         value: true,
       });
@@ -461,7 +461,7 @@
       /***/
     },
 
-    /***/ 6509: /***/ (
+    /***/ 4645: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -475,9 +475,9 @@
           return getImgProps;
         },
       });
-      const _warnonce = __webpack_require__(7147);
-      const _imageblursvg = __webpack_require__(5901);
-      const _imageconfig = __webpack_require__(1545);
+      const _warnonce = __webpack_require__(3975);
+      const _imageblursvg = __webpack_require__(3749);
+      const _imageconfig = __webpack_require__(8661);
       const VALID_LOADING_VALUES =
         /* unused pure expression or super */ null && [
           "lazy",
@@ -850,8 +850,8 @@
       /***/
     },
 
-    /***/ 4591: /***/ (module, exports, __webpack_require__) => {
-      /* provided dependency */ var process = __webpack_require__(4784);
+    /***/ 3039: /***/ (module, exports, __webpack_require__) => {
+      /* provided dependency */ var process = __webpack_require__(1482);
       /* __next_internal_client_entry_do_not_use__  cjs */
       Object.defineProperty(exports, "__esModule", {
         value: true,
@@ -872,19 +872,19 @@
           return defaultHead;
         },
       });
-      const _interop_require_default = __webpack_require__(3280);
-      const _interop_require_wildcard = __webpack_require__(8464);
-      const _jsxruntime = __webpack_require__(673);
+      const _interop_require_default = __webpack_require__(9218);
+      const _interop_require_wildcard = __webpack_require__(8553);
+      const _jsxruntime = __webpack_require__(9348);
       const _react = /*#__PURE__*/ _interop_require_wildcard._(
-        __webpack_require__(254)
+        __webpack_require__(8196)
       );
       const _sideeffect = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(31)
+        __webpack_require__(2968)
       );
-      const _ampcontextsharedruntime = __webpack_require__(5225);
-      const _headmanagercontextsharedruntime = __webpack_require__(3382);
-      const _ampmode = __webpack_require__(4457);
-      const _warnonce = __webpack_require__(7147);
+      const _ampcontextsharedruntime = __webpack_require__(4551);
+      const _headmanagercontextsharedruntime = __webpack_require__(452);
+      const _ampmode = __webpack_require__(7094);
+      const _warnonce = __webpack_require__(3975);
       function defaultHead(inAmpMode) {
         if (inAmpMode === void 0) inAmpMode = false;
         const head = [
@@ -1068,7 +1068,7 @@
       /***/
     },
 
-    /***/ 5901: /***/ (__unused_webpack_module, exports) => {
+    /***/ 3749: /***/ (__unused_webpack_module, exports) => {
       /**
        * A shared function, used on both client and server, to generate a SVG blur placeholder.
        */
@@ -1122,7 +1122,7 @@
       /***/
     },
 
-    /***/ 9041: /***/ (
+    /***/ 5611: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -1136,11 +1136,11 @@
           return ImageConfigContext;
         },
       });
-      const _interop_require_default = __webpack_require__(3280);
+      const _interop_require_default = __webpack_require__(9218);
       const _react = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(254)
+        __webpack_require__(8196)
       );
-      const _imageconfig = __webpack_require__(1545);
+      const _imageconfig = __webpack_require__(8661);
       const ImageConfigContext = _react.default.createContext(
         _imageconfig.imageConfigDefault
       );
@@ -1150,7 +1150,7 @@
       /***/
     },
 
-    /***/ 1545: /***/ (__unused_webpack_module, exports) => {
+    /***/ 8661: /***/ (__unused_webpack_module, exports) => {
       Object.defineProperty(exports, "__esModule", {
         value: true,
       });
@@ -1198,7 +1198,7 @@
       /***/
     },
 
-    /***/ 4980: /***/ (__unused_webpack_module, exports) => {
+    /***/ 9206: /***/ (__unused_webpack_module, exports) => {
       Object.defineProperty(exports, "__esModule", {
         value: true,
       });
@@ -1231,7 +1231,7 @@
       /***/
     },
 
-    /***/ 7112: /***/ (
+    /***/ 4332: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -1245,9 +1245,9 @@
           return RouterContext;
         },
       });
-      const _interop_require_default = __webpack_require__(3280);
+      const _interop_require_default = __webpack_require__(9218);
       const _react = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(254)
+        __webpack_require__(8196)
       );
       const RouterContext = _react.default.createContext(null);
       if (false) {
@@ -1256,7 +1256,11 @@
       /***/
     },
 
-    /***/ 31: /***/ (__unused_webpack_module, exports, __webpack_require__) => {
+    /***/ 2968: /***/ (
+      __unused_webpack_module,
+      exports,
+      __webpack_require__
+    ) => {
       Object.defineProperty(exports, "__esModule", {
         value: true,
       });
@@ -1266,7 +1270,7 @@
           return SideEffect;
         },
       });
-      const _react = __webpack_require__(254);
+      const _react = __webpack_require__(8196);
       const isServer = typeof window === "undefined";
       const useClientOnlyLayoutEffect = isServer
         ? () => {}
Diff for 3463-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 pages-turbo...time.prod.js

Diff too large to display

Diff for pages.runtime.dev.js

Diff too large to display

Diff for pages.runtime.prod.js

Diff too large to display

Diff for server.runtime.prod.js

Diff too large to display

Commit: d154bd5

@feedthejim
Copy link
Contributor

Note that this should address some of the concerns in #46722.

Great work @unstubbable 👋.

@bookernath
Copy link

bookernath commented Oct 9, 2024

Love this 👏

Do you envision support for rewrite() through this mechanism in the future, or are there good reasons for rewrites to remain exclusive to edge middleware?

Our use case today uses middleware for URL virtualization for cases where URLs need to break the rules of file-system-based routing. We depend on outbound API calls to do this, so we run into issues with the performance of middleware in such cases and the lack of cache.

Moving this down into an interceptor as part of a catchall route could be a great solution, but it depends on rewrite().

@pilcrowonpaper
Copy link

pilcrowonpaper commented Oct 9, 2024

By design, since Interceptors don't prevent the initial shell from being served to users, they cannot change the response status code or headers.

This seems like a missed opportunity to address some real issues with Next.js. Is it not possible to allow devs to define when to start the streaming process?

export default async function intercept(request: NextRequest, startRender: Function) {
  // ... Can set headers and even return custom response, like in middleware.ts

  // server initial shell
  startRender();

  // ... do analytics stuff that doesn't require modifying the response headers
}

@colinclerk
Copy link
Contributor

Thank you for your work here 🙏! We're excited to add interceptor support to Clerk

We have a few questions we'd appreciate your thoughts on:

Can you clarify how it's decided if dashboard/interceptor.ts runs in front of a server action? Is it based on the URL a server action is invoked from? Does it matter at all where/how the server action is defined?

--

It sounds like dashboard/layout.tsx will start rendering before dashboard/interceptor.ts can trigger a redirect, therefore increasing Cumulative Layout Shift unless redirecting to a page with the same layout. This was chosen to optimize FCP, and the PR suggests we continue using middleware if we prefer to optimize CLS. Am I understanding that tradeoff correctly?

If so, I think we'd prefer dashboard/interceptor.ts deferred the rendering of dashboard/layout.tsx, to avoid CLS at the expense of FCP. We don't expect an interceptor to take more than single-digit milliseconds, so it wouldn't have a meaningful impact on FCP. If that was a concern, we could potentially leverage a route group to ensure the layout is rendered in front of the interceptor.

Notably: We're heavily indexed on the auth case, and we think it's nearly guaranteed that /sign-in will not share a layout with authenticated pages, so we'd anticipate quite a bit of CLS introduced by this.

--

We are curious if more information is available about notAuthorized()? We are wondering if we'll be able to pass more granular reasons into this (e.g. does not have access, needs to re-authenticate ahead of sensitive action)

Thank you!

@feedthejim
Copy link
Contributor

feedthejim commented Oct 9, 2024

@bookernath

Do you envision support for rewrite() through this mechanism in the future, or are there good reasons for rewrites to remain exclusive to edge middleware?

We don't envision supporting rewrites through that API. This is one of the reasons this API is complementary to the middleware and not a replacement.

Ideally, a rewrite needs to be done at the edge/closest to the user to avoid the cost of a potentially heavy roundtrip between the origin and the rewrite destination.

We depend on outbound API calls to do this, so we run into issues with the performance of middleware in such cases and the lack of cache.

That's interesting. In this case, what is blocking you from adding a caching layer?

@feedthejim
Copy link
Contributor

feedthejim commented Oct 9, 2024

@pilcrowonpaper

This seems like a missed opportunity to address some real issues with Next.js. Is it not possible to allow devs to define when to start the streaming process?

The design is intentionally limited because this is not a replacement for the Edge Middleware.

What is not explicitly said in this PR is that we're also working on planning to add support for the Node.js Middleware to unlock some of the DX issues you may have been encountering.

@feedthejim
Copy link
Contributor

@colinclerk

Can you clarify how it's decided if dashboard/interceptor.ts runs in front of a server action? Is it based on the URL a server action is invoked from? Does it matter at all where/how the server action is defined?

This is not part of the PR and we're still figuring out the semantics there. We discussed an filesystem-based approach but still TBD.

It sounds like dashboard/layout.tsx will start rendering before dashboard/interceptor.ts can trigger a redirect, therefore increasing Cumulative Layout Shift unless redirecting to a page with the same layout. This was chosen to optimize FCP, and the PR suggests we continue using middleware if we prefer to optimize CLS. Am I understanding that tradeoff correctly?

yes, depending on your website design, it would make CLS worst.

We are curious if more information is available about notAuthorized()? We are wondering if we'll be able to pass more granular reasons into this (e.g. does not have access, needs to re-authenticate ahead of sensitive action)

Not yet, we've yet to start it but that sounds like a reasonable ask.

@bookernath
Copy link

That's interesting. In this case, what is blocking you from adding a caching layer?

We've added a cache with Vercel KV, but as that also requires an outbound HTTP call from the edge to access the cache, performance is sometimes 100ms+ to fetch from KV cache at the edge which defeats the point of a cache.

Edge Config's limits don't meet our requirements either in terms of size and rate-of-update.

I'm happy to discuss this further in a different forum as now that I have my answer on rewrite() (thank you) I feel this is off topic for this PR.

@ijjk ijjk added the examples Issue was opened via the examples template. label Oct 9, 2024
@eric-burel
Copy link
Contributor

eric-burel commented Oct 10, 2024

This seems to be a major improvement to Next.js, solving many pain points, congrats on this proposal!

Will an interceptor be the right place :

Edit:

"We don't envision supporting rewrites through that API. This is one of the reasons this API is complementary to the middleware and not a replacement."

That could be a problem for implementing a static paywall or any kind of static personalization that needs a database call (AB testing beyond a simple modulo, marketing segmentation etc.). URL rewrites + the ability to call a database unlocks a significant number of use cases, that are complementary to edge middlewares.

  • To establish a persistent database connection? I understand that in the end you may have 1 connection per page or API route in a serverless paradigm, and that interceptors won't change that if they claim to live in the same process as the page. But I'd be eager to simplify the code, by putting it in a top-level interceptor, rather than having a checkDatabaseConnection() on absolutely every page/every database method calling

@LFCavalcanti
Copy link

LFCavalcanti commented Oct 10, 2024

So, we are circling back to the pattern of having route based middleware, like Express does and like it was in Next, includding the AppRouter at some point in development.

Caveats

By design, since Interceptors don't prevent the initial shell from being served to users, they cannot change the response status code or headers. Setting cookies needs to be done either in Middleware, server actions or route handlers instead.

This part of not preventing the initial shell of the application can be a security concern.

And the second part about Cookies got me confused, is explicitly stated that in the Middleware does not allow manipulation of Cookies:
[Error: Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options]

So these interceptors should be able to handle Cookies, or that problem will not be solved.


I would really appreciate if someone from the core team could directly address why we still don't have an option to execute the middleware with the Node standard runtime... It feels like the decisions to steer the Framework are disregarding use cases outside the "Serveless" sphere, that is not a solution to every problem.

Edit: Toning down some vented frustration.

@marko-hologram
Copy link

marko-hologram commented Oct 10, 2024

So, we are circling back to the pattern of having route based middleware, like Express does and like it was in Next, includding the AppRouter at some point in development.

Yeah I remember when you could nest _middleware files inside directories and it would apply that middleware from that route down to everything on same level and below it. Liked that pattern, but that still prevented sharing some context between middleware if I remember correctly. For example I couldn't just get currently logged in user data globally and then use that in other middleware to do some checks. For example check user role access for something.

And the second part about Cookies got me confused, is explicitly stated that in the Middleware does not allow manipulation of Cookies: [Error: Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options]

Cookies can absolutely be manipulated in middleware. They have an example in the docs where they are setting a cookie on the response inside middleware https://nextjs.org/docs/app/building-your-application/routing/middleware#using-cookies. That error message might need to be adjusted to reflect that or maybe that error message was leaving that out because cookies() function isn't present in middleware and I'm guessing this error was thrown when cookies() is used.

I would really appreciate if someone from the core team could directly address why we still don't have an option to execute the middleware with the Node standard runtime

As far as I know, they stated multiple times now that they are looking into expanding middleware to be able to use Node.js runtime instead of limited Edge Runtime. @feedthejim can verify that I think. I think I've seen him mention it on Twitter somewhere, but a confirmation here would be super appreciated since interceptors are kinda tied to middleware (at least conceptually).

EDIT: Sorry Jimmy, tagged you unnecessarily about this when few messages above you did mention "What is not explicitly said in this PR is that we're also working on planning to add support for the Node.js Middleware to unlock some of the DX issues you may have been encountering.". Thanks for mentioning this 😄

@LFCavalcanti
Copy link

LFCavalcanti commented Oct 10, 2024

So, we are circling back to the pattern of having route based middleware, like Express does and like it was in Next, includding the AppRouter at some point in development.

Yeah I remember when you could nest _middleware files inside directories and it would apply that middleware from that route down to everything on same level and below it. Liked that pattern, but that still prevented sharing some context between middleware if I remember correctly. For example I couldn't just get currently logged in user data globally and then use that in other middleware to do some checks. For example check user role access for something.

Yes, but that pattern was less messy, you could implemented only in the routes where you wanted to secure with session authentication, without config match and loads of code in one file.

And the second part about Cookies got me confused, is explicitly stated that in the Middleware does not allow manipulation of Cookies: [Error: Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options]

Cookies can absolutely be manipulated in middleware. They have an example in the docs where they are setting a cookie on the response inside middleware https://nextjs.org/docs/app/building-your-application/routing/middleware#using-cookies. That error message might need to be adjusted to reflect that or maybe that error message was leaving that out because cookies() function isn't present in middleware and I'm guessing this error was thrown when cookies() is used.

I arrived at that solution, not using their documentation though. Defining a set-Cookie header in the Response of the Middleware that will be sent to the route and the response of the route send it back to the client... it seems counter intuitive.

I had to get a better understaing from a @leerob video and some questions from Stack Overflow.

I would really appreciate if someone from the core team could directly address why we still don't have an option to execute the middleware with the Node standard runtime

As far as I know, they stated multiple times now that they are looking into expanding middleware to be able to use Node.js runtime instead of limited Edge Runtime. @feedthejim can verify that I think. I think I've seen him mention it on Twitter somewhere, but a confirmation here would be super appreciated since interceptors are kinda tied to middleware (at least conceptually).

Yep, Interceptors solve some use cases, but the whole issue with auth when using a non cloud Database will endure. It's like Isildur cutting the One Ring from Sauron's hand, but not tossing it into Mount Doom.

@marko-hologram
Copy link

I arrived at that solution, not using their documentation though

To be honest, documentation should be the primary learning resource for a tool like Next.js. We should all read it before jumping to conclusions, myself included. Can it be improved? Yes, all documentation can be improved.

Defining a set-Cookie header in the Response of the Middleware that will be sent to the route and the response of the route send it back to the client... it seems counter intuitive.

We all have different mental models in our heads, but this wasn't that counter intuitive for me. Middleware just modifies the response that will be sent and that's it. No need to read into it too much. I believe that's how middleware in most (all?) frameworks can be used.


But anyway, let us try to be good OSS citizens and not make this PR extremely off-topic and try to focus on the feature being implemented here 😄

Now that I'm here anyway, I'd like to share some thoughts.

Integrate with the app router file conventions instead of using a matcher config.

I like this part. Currently middleware code can look awful if you do anything except most trivial middleware such as a single if/else check shown in example code that checks if session cookie exists. I had to reach for a library (https://nemo.rescale.build/) to make middleware friendlier to use and to get other benefits such as chaining middleware or global middleware that runs before other middleware to set some "context" between them. Would love a 1st party solution to this. I know you don't want users to run a lot of code in middleware, but if I want to do it, let me do it anyway with a friendlier API.

Note that we’ve located the sign-in page inside app/dashboard. You don’t have to do this, but it avoids layout thrashing and UI flashes when redirecting logged-out users from the dashboard shell to the sign-in page, because both pages then share a layout.

Personally, this part communicated well what I don't like about this proposal. Part of the UI will be served and then after some check, the user might be redirected to another UI. That seems like awful UX to me. Might as well just go back to a fully client rendered SPA because that experience is the same there. Yes, a quick check can be done in middleware to redirect early, but what if redirect depends on some permissions or user role? If done in interceptor, UI flashing might happen and I need to re-organize my UI now to adapt to this API to prevent this.

Interceptors should therefore be efficient to avoid unnecessary delays and impacting response times

How is this different compared to middleware needing to be efficient and fast? What would stop someone from just putting an interceptor at the root of /app folder and having a global interceptor that is slow and impacting everything? If one of the primary goals is "full access to Node.js APIs", why not just improve middleware instead of introducing yet another API we have to learn?

Overall, I'm not seeing what problems does this API actually solve and would love to see this effort being put into making existing middleware API better. I'm 100% sure that I'm missing something here, but the only thing here that I really do appreciate is the file system aspect of it and being able to co-locate this code with the route segments it's targeting. As mentioned in the example, to target everything at /dashboard routes for example it's nicer to put the file in a folder, compared to doing checks in middleware in case you have bunch of other routes that have specific behavior.

Maybe when some time passes and I use this extensively will I see the actual benefits 😄 For now, I'd prefer if this effort was put into improving existing APIs instead of creating new ones.

@LFCavalcanti
Copy link

I arrived at that solution, not using their documentation though

To be honest, documentation should be the primary learning resource for a tool like Next.js. We should all read it before jumping to conclusions, myself included. Can it be improved? Yes, all documentation can be improved.

Just to clarify... I did read the documentation when I got the error message, but the documentation doesn't explain the full workflow, so you have to figure out parts of the solution without having a good understanding of what the Framework will to on it's own to deliver the Cookie to the Client(User's Browser), more on that bellow.

Defining a set-Cookie header in the Response of the Middleware that will be sent to the route and the response of the route send it back to the client... it seems counter intuitive.

We all have different mental models in our heads, but this wasn't that counter intuitive for me. Middleware just modifies the response that will be sent and that's it. No need to read into it too much. I believe that's how middleware in most (all?) frameworks can be used.

The mental model of what the Middleware does was not the main issue for me... it was understanding that Next.js detects a "Set-Cookie" header in the Middleware response when running the Route being called and puts the same header in the Route response so the User's Browser set's it.

Maybe I'm suposed to know this is standard behavior... If it is standard let me know I'll do a deep dive in the subject.

If not, is there a way to submit suggestions to their documentation? I'm in a crunch now, but I would be happy to produce an example and a workflow chart for them to have a section in the Middleware Docs.

I just don't want to put the work into it and have it lost into the void of the "feedback form".

Overall, I'm not seeing what problems does this API actually solve and would love to see this effort being put into making existing middleware API better. I'm 100% sure that I'm missing something here, but the only thing here that I really do appreciate is the file system aspect of it and being able to co-locate this code with the route segments it's targeting. As mentioned in the example, to target everything at /dashboard routes for example it's nicer to put the file in a folder, compared to doing checks in middleware in case you have bunch of other routes that have specific behavior.

They could solve this having the Global Middleware run in the Edge Runtime and Route Middlewares run in the same runtime as the routes, like a Decorator? IMO seems a simpler solution.

@amannn
Copy link
Contributor

amannn commented Oct 14, 2024

Really cool to see this proposal! 🙌

Interceptors will opt pages into dynamic rendering.

I guess that's because the request is passed to the function, so it's assumed to be dynamic—right?

I was wondering if interceptors could be a good place to validate segments like [locale] (instead of repeating the validation in a layout, page, metadata etc.). For this case, it could be interesting if interceptors wouldn't opt into dynamic rendering, in case only a route param is read (no request info).

Was this considered for the design of this feature?

EDIT: It seems like dynamicParams = false is actually a reasonable solution for validating a locale param—at least if locales are known statically.

EDIT 2: dynamicParams is not compatible with dynamicIO (Next.js prints this as an error). Not sure if that's a temporary restriction, but unfortunately this defeats using dynamicParams for validating the locale segment.

EDIT 3: It seems like the Next.js team is working on an alternative to dynamicParams = false.

@eric-burel
Copy link
Contributor

eric-burel commented Oct 14, 2024

Interceptors will opt pages into dynamic rendering. Therefore, they're best suited to intercept personalized routes

I've been advocating that static rendering should not be opposed to personnalisation for a few years now, and this starts being the norm in the ecosystem :

So I am tad disappointed by this constraint :( It will make it useless for building paywall or auth against static content, like private documentation or such.
Is there a solid technical reason for opting pages into dynamic rendering when using a request interceptor?

@sanny-io
Copy link

sanny-io commented Oct 16, 2024

I don't think it's good DX to have this much overlap between two discrete features that are conceptually very similar, but differ in very subtle ways. I understand how the following snippets are different, but I'm sure you can imagine how it wouldn't feel good to maintain, especially as app complexity grows.

They're the same. But they're also different. And they should also be used together at the same time.

import { auth } from '@/auth';
import { redirect } from 'next/navigation';

const signInPathname = '/dashboard/sign-in';

export default async function intercept(request: NextRequest): Promise<void> {
  const session = await auth();

  if (!session && request.nextUrl.pathname !== signInPathname) {
    redirect(signInPathname);
  }
}
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const signInPathname = '/dashboard/sign-in';

export function middleware(request: NextRequest): NextResponse {
  if (
    !request.nextUrl.pathname.startsWith(signInPathname) &&
    !request.cookies.has('session')
  ) {
    return NextResponse.redirect(new URL(signInPathname, request.url));
  }

  return NextResponse.next();
}

export const config = { matcher: ['/dashboard/:path*'] };

@typeofweb
Copy link
Contributor

This is great! We can’t wait to test it. Is there a timeline to get this to a canary (or beta, nightly, whatever makes sense)?

@themagickoala
Copy link

I'm very keen on this use case for various sub-routes of my application!

@tomwanzek
Copy link

First of all, thanks a lot for floating this proposal to address the current Next.js middleware granularity gap 🙇

@colinclerk

Can you clarify how it's decided if dashboard/interceptor.ts runs in front of a server action? Is it based on the URL a server action is invoked from? Does it matter at all where/how the server action is defined?

This is not part of the PR and we're still figuring out the semantics there. We discussed an filesystem-based approach but still TBD.

I'm wondering about the above mentioned reference to discussing a filesystem-based approach for this specific solution design aspect. I understand that Next.js traditionally took a filesystem-based approach for it's routing tree definition (unlike other React and non-React frameworks with programmatic/virtual route config definitions.

If I read the above exploration correctly, that would imply that server actions would have to be collocated (at the appropriate route tree depth level within the app/... page/layout/interceptor tree. Given that the server actions are likely some of the most sensitive code (security, backend system facing etc.), I believe there is/was always a reasonable case to separate them out from the app/... directory into e.g. a sibling /server/... directory (if not even separate data access layer package?) organized by server-side feature/domain/service. I.e. a more tightly guarded part of the code base constituting the data access layer. The obvious benefits being:

  • possibly separate/different code ownership (PR review requirements/controls) in larger projects/teams
  • Reuse across layouts/pages/components

These aspects seem to outweigh colocating server actions within the FE page/component tree part of the code base, to me anyway.

At a minimum, it seems, it would force an additional layer of indirection in code organization, if Next.js adopted a file-based convention to force server actions into page/layout/interceptor tree. To achieve the above benefits, one would then have to treat them as just a facade wrapping around the actual data access layer code with the latter still be kept in the separate part of the code base and just invoked inside the server action 🤔

Not sure, if I read too much into this innocent part of the implementation discussion, however, I felt like calling it out as a consideration.

@unstubbable
Copy link
Contributor Author

unstubbable commented Oct 25, 2024

@tomwanzek I think this is a misunderstanding. Jimmy didn't specifically answer this question by Colin:

Is it based on the URL a server action is invoked from?

The answer to that is: yes, this is how it's currently implemented in this draft. A filesystem approach based on where actions are located in the filesystem would be something on top of that, that we might or might not add in a future iteration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
created-by: Next.js team PRs by the Next.js team. examples Issue was opened via the examples template. tests Turbopack Related to Turbopack with Next.js. type: next
Projects
None yet
Development

Successfully merging this pull request may close these issues.