-
Notifications
You must be signed in to change notification settings - Fork 27.2k
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
base: canary
Are you sure you want to change the base?
Conversation
Tests Passed |
Stats from current PRDefault Build (Increase detected
|
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 | |
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 | |
page.js gzip | 187 kB | 188 kB | |
Overall change | 316 kB | 317 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 | |
edge-runtime..pack.js gzip | 844 B | 844 B | ✓ |
Overall change | 31.1 kB | 31.6 kB |
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 | |
app-page-exp..prod.js gzip | 119 kB | 121 kB | |
app-page-tur..prod.js gzip | 133 kB | 134 kB | |
app-page-tur..prod.js gzip | 128 kB | 129 kB | |
app-page.run...dev.js gzip | 300 kB | 302 kB | |
app-page.run..prod.js gzip | 115 kB | 116 kB | |
app-route-ex...dev.js gzip | 34.4 kB | 36.1 kB | |
app-route-ex..prod.js gzip | 23.3 kB | 24.6 kB | |
app-route-tu..prod.js gzip | 23.3 kB | 24.6 kB | |
app-route-tu..prod.js gzip | 23.1 kB | 24.5 kB | |
app-route.ru...dev.js gzip | 36 kB | 37.7 kB | |
app-route.ru..prod.js gzip | 23.1 kB | 24.5 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 | |
pages.runtim...dev.js gzip | 26.5 kB | 27.4 kB | |
pages.runtim..prod.js gzip | 20.9 kB | 21.6 kB | |
server.runti..prod.js gzip | 59.1 kB | 59.3 kB | |
Overall change | 1.43 MB | 1.45 MB |
build cache Overall increase ⚠️
vercel/next.js canary | vercel/next.js interceptors | Change | |
---|---|---|---|
0.pack gzip | 1.84 MB | 1.84 MB | |
index.pack gzip | 143 kB | 143 kB | N/A |
Overall change | 1.84 MB | 1.84 MB |
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
1de4954
to
76f53ad
Compare
Note that this should address some of the concerns in #46722. Great work @unstubbable 👋. |
Love this 👏 Do you envision support for 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 |
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
} |
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 -- It sounds like If so, I think we'd prefer 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 Thank you! |
15443f7
to
39af6f6
Compare
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.
That's interesting. In this case, what is blocking you from adding a caching layer? |
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. |
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.
yes, depending on your website design, it would make CLS worst.
Not yet, we've yet to start it but that sounds like a reasonable ask. |
39af6f6
to
63e8675
Compare
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 |
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:
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.
|
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.
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: 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. |
Yeah I remember when you could nest
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.
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 😄 |
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.
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.
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. |
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.
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.
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.
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.
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 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 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. |
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.
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".
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. |
Really cool to see this proposal! 🙌
I guess that's because the I was wondering if interceptors could be a good place to validate segments like Was this considered for the design of this feature? EDIT: It seems like EDIT 2: EDIT 3: It seems like the Next.js team is working on an alternative to |
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. |
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*'] }; |
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)? |
I'm very keen on this use case for various sub-routes of my application! |
First of all, thanks a lot for floating this proposal to address the current Next.js middleware granularity gap 🙇
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
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. |
@tomwanzek I think this is a misunderstanding. Jimmy didn't specifically answer this question by Colin:
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. |
@unstubbable was it considered to revert back to nested middleware? To me, it feels directionally wrong to have yet another API. This feels like it partially overlaps with middleware and simultaneously with layout/template files. Nested middleware was, IMO, a much more composable solution that did not require understanding multiple competing conventions. Nested middleware also has the benefit of working perfectly with static pages. |
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
Background
Next.js Middleware allows running code before a request is processed but has limitations:
fs
,net
,child_process
,crypto
, and others.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 theapp
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
totrue
in yournext.config.ts
file.An Interceptor exports a default asynchronous function that receives a
NextRequest
object: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:With
lib/auth.ts
looking something like this:This assumes that there’s also
app/dashboard/layout.tsx
andapp/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: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