From 154b340fe31dc338fa17def69fa334db2501cb22 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 7 Oct 2025 14:22:42 +0200 Subject: [PATCH 1/5] Skip internal routes in middleware --- crates/next-api/src/middleware.rs | 35 ++++++++++-- crates/next-core/src/next_config.rs | 6 +++ packages/next/src/build/entries.ts | 3 +- packages/next/src/build/index.ts | 6 +-- packages/next/src/build/webpack-config.ts | 1 + .../webpack/plugins/middleware-plugin.ts | 54 +++++++++++++------ packages/next/src/server/config-schema.ts | 1 + packages/next/src/server/config-shared.ts | 7 +++ .../src/server/lib/router-utils/filesystem.ts | 3 +- .../lib/router-utils/setup-dev-bundler.ts | 3 +- packages/next/src/server/next-server.ts | 12 +++-- .../utils/get-default-middleware-matcher.ts | 21 ++++++++ 12 files changed, 118 insertions(+), 34 deletions(-) create mode 100644 packages/next/src/shared/lib/router/utils/get-default-middleware-matcher.ts diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index 63304b4b9d3f47..696512a4078bd8 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -42,6 +42,31 @@ use crate::{ route::{Endpoint, EndpointOutput, EndpointOutputPaths}, }; +/// Rust implementation of the TypeScript getDefaultMiddlewareMatcher function +/// Generates default middleware matcher patterns that respect skipMiddlewareNextInternalRoutes +fn get_default_middleware_matcher( + skip_middleware_next_internal_routes: Option, +) -> MiddlewareMatcher { + let skip_internal = skip_middleware_next_internal_routes.unwrap_or(true); + + if skip_internal { + // Skip "/_next/" internal routes, except for "/_next/data/" which is needed for + // client-side navigation. Do not consider basePath as the user cannot create a + // route starts with underscore. + MiddlewareMatcher { + regexp: Some(rcstr!("^(?!.*\\/\\_next\\/(?!data\\/)).*")), + original_source: rcstr!("/:path*"), + ..Default::default() + } + } else { + MiddlewareMatcher { + regexp: Some(rcstr!("^/.*$")), + original_source: rcstr!("/:path*"), + ..Default::default() + } + } +} + #[turbo_tasks::value] pub struct MiddlewareEndpoint { project: ResolvedVc, @@ -165,6 +190,8 @@ impl MiddlewareEndpoint { .map(|i18n| i18n.locales.len() > 1) .unwrap_or(false); let base_path = next_config.base_path().await?; + let skip_middleware_next_internal_routes = + next_config.skip_middleware_next_internal_routes().await?; let matchers = if let Some(matchers) = config.middleware_matcher.as_ref() { matchers @@ -216,11 +243,9 @@ impl MiddlewareEndpoint { }) .collect() } else { - vec![MiddlewareMatcher { - regexp: Some(rcstr!("^/.*$")), - original_source: rcstr!("/:path*"), - ..Default::default() - }] + vec![get_default_middleware_matcher( + *skip_middleware_next_internal_routes, + )] }; if matches!(runtime, NextRuntime::NodeJs) { diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 29d625b90c097c..4d0b20600c8c6f 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -103,6 +103,7 @@ pub struct NextConfig { asset_prefix: Option, base_path: Option, skip_middleware_url_normalize: Option, + skip_middleware_next_internal_routes: Option, skip_trailing_slash_redirect: Option, i18n: Option, cross_origin: Option, @@ -1343,6 +1344,11 @@ impl NextConfig { Vc::cell(self.base_path.clone()) } + #[turbo_tasks::function] + pub fn skip_middleware_next_internal_routes(&self) -> Vc> { + Vc::cell(self.skip_middleware_next_internal_routes) + } + #[turbo_tasks::function] pub fn cache_handler(&self, project_path: FileSystemPath) -> Result> { if let Some(handler) = &self.cache_handler { diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index ccd03f221f8cfd..2927325f0d88c8 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -45,6 +45,7 @@ import { isInstrumentationHookFilename, } from './utils' import { getPageStaticInfo } from './analysis/get-page-static-info' +import { getDefaultMiddlewareMatcher } from '../shared/lib/router/utils/get-default-middleware-matcher' import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import type { ServerRuntime } from '../types' @@ -898,7 +899,7 @@ export async function createEntrypoints( if (isMiddlewareFile(page)) { middlewareMatchers = staticInfo.middleware?.matchers ?? [ - { regexp: '.*', originalSource: '/:path*' }, + getDefaultMiddlewareMatcher(params.config), ] } diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index fd059406d324ad..735b75db15fc59 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -150,6 +150,7 @@ import { isEdgeRuntime } from '../lib/is-edge-runtime' import { recursiveCopy } from '../lib/recursive-copy' import { lockfilePatchPromise, teardownTraceSubscriber } from './swc' import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex' +import { getDefaultMiddlewareMatcher } from '../shared/lib/router/utils/get-default-middleware-matcher' import { getFilesInDir } from '../lib/get-files-in-dir' import { eventSwcPlugins } from '../telemetry/events/swc-plugins' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' @@ -2590,10 +2591,7 @@ export default async function build( functionsConfigManifest.functions['/_middleware'] = { runtime: staticInfo.runtime, matchers: staticInfo.middleware?.matchers ?? [ - { - regexp: '^.*$', - originalSource: '/:path*', - }, + getDefaultMiddlewareMatcher(config), ], } diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 04b97894050939..09e65bdc933bd8 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -2081,6 +2081,7 @@ export default async function getBaseWebpackConfig( dev, sriEnabled: !dev && !!config.experimental.sri?.algorithm, rewrites, + nextConfig: config, edgeEnvironments: { __NEXT_BUILD_ID: buildId, NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: encryptionKey, diff --git a/packages/next/src/build/webpack/plugins/middleware-plugin.ts b/packages/next/src/build/webpack/plugins/middleware-plugin.ts index 3f36ac8b09026a..1d78740f0e0b9c 100644 --- a/packages/next/src/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/src/build/webpack/plugins/middleware-plugin.ts @@ -5,6 +5,7 @@ import type { import type { EdgeSSRMeta } from '../loaders/get-module-build-info' import type { MiddlewareMatcher } from '../../analysis/get-page-static-info' import { getNamedMiddlewareRegex } from '../../../shared/lib/router/utils/route-regex' +import { getDefaultMiddlewareMatcher } from '../../../shared/lib/router/utils/get-default-middleware-matcher' import { getModuleBuildInfo } from '../loaders/get-module-build-info' import { getSortedRoutes } from '../../../shared/lib/router/utils' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' @@ -36,6 +37,7 @@ import type { CustomRoutes } from '../../../lib/load-custom-routes' import { isInterceptionRouteRewrite } from '../../../lib/generate-interception-routes-rewrites' import { getDynamicCodeEvaluationError } from './wellknown-errors-plugin/parse-dynamic-code-evaluation-error' import { getModuleReferencesInOrder } from '../utils' +import type { NextConfigComplete } from '../../../server/config-shared' const KNOWN_SAFE_DYNAMIC_PACKAGES = require('../../../lib/known-edge-safe-packages.json') as string[] @@ -195,21 +197,29 @@ function getCreateAssets(params: { continue } - const matcherSource = metadata.edgeSSR?.isAppDir - ? normalizeAppPath(page) - : page - - const catchAll = !metadata.edgeSSR && !metadata.edgeApiFunction - - const { namedRegex } = getNamedMiddlewareRegex(matcherSource, { - catchAll, - }) - const matchers = metadata?.edgeMiddleware?.matchers ?? [ - { - regexp: namedRegex, - originalSource: page === '/' && catchAll ? '/:path*' : matcherSource, - }, - ] + let matchers: MiddlewareMatcher[] + if (metadata?.edgeMiddleware?.matchers) { + matchers = metadata.edgeMiddleware.matchers + } else { + // For middleware at root with no explicit matchers, use getDefaultMiddlewareMatcher + // which respects skipMiddlewareNextInternalRoutes config + const catchAll = !metadata.edgeSSR && !metadata.edgeApiFunction + if (page === '/' && catchAll) { + matchers = [getDefaultMiddlewareMatcher(opts.nextConfig)] + } else { + const matcherSource = metadata.edgeSSR?.isAppDir + ? normalizeAppPath(page) + : page + matchers = [ + { + regexp: getNamedMiddlewareRegex(matcherSource, { + catchAll, + }).namedRegex, + originalSource: matcherSource, + }, + ] + } + } const isEdgeFunction = !!(metadata.edgeApiFunction || metadata.edgeSSR) const edgeFunctionDefinition: EdgeFunctionDefinition = { @@ -818,6 +828,7 @@ interface Options { sriEnabled: boolean rewrites: CustomRoutes['rewrites'] edgeEnvironments: EdgeRuntimeEnvironments + nextConfig: NextConfigComplete } export default class MiddlewarePlugin { @@ -825,12 +836,20 @@ export default class MiddlewarePlugin { private readonly sriEnabled: Options['sriEnabled'] private readonly rewrites: Options['rewrites'] private readonly edgeEnvironments: EdgeRuntimeEnvironments - - constructor({ dev, sriEnabled, rewrites, edgeEnvironments }: Options) { + private readonly nextConfig: Options['nextConfig'] + + constructor({ + dev, + sriEnabled, + rewrites, + edgeEnvironments, + nextConfig, + }: Options) { this.dev = dev this.sriEnabled = sriEnabled this.rewrites = rewrites this.edgeEnvironments = edgeEnvironments + this.nextConfig = nextConfig } public apply(compiler: webpack.Compiler) { @@ -886,6 +905,7 @@ export default class MiddlewarePlugin { rewrites: this.rewrites, edgeEnvironments: this.edgeEnvironments, dev: this.dev, + nextConfig: this.nextConfig, }, }) ) diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 31ddf66d65a7c9..072a0c4abcd8c3 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -677,6 +677,7 @@ export const configSchema: zod.ZodType = z.lazy(() => serverExternalPackages: z.array(z.string()).optional(), serverRuntimeConfig: z.record(z.string(), z.any()).optional(), skipMiddlewareUrlNormalize: z.boolean().optional(), + skipMiddlewareNextInternalRoutes: z.boolean().optional(), skipTrailingSlashRedirect: z.boolean().optional(), staticPageGenerationTimeout: z.number().optional(), expireTime: z.number().optional(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 5ec7cf193f2cac..644c7c93cd3e2e 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -1249,6 +1249,12 @@ export interface NextConfig { skipMiddlewareUrlNormalize?: boolean + /** + * Skip Next.js internals route `/_next` from middleware. + * @default true + */ + skipMiddlewareNextInternalRoutes?: boolean + skipTrailingSlashRedirect?: boolean modularizeImports?: Record< @@ -1529,6 +1535,7 @@ export const defaultConfig = Object.freeze({ }, htmlLimitedBots: undefined, bundlePagesRouterDependencies: false, + skipMiddlewareNextInternalRoutes: true, } satisfies NextConfig) export async function normalizeConfig(phase: string, config: any) { diff --git a/packages/next/src/server/lib/router-utils/filesystem.ts b/packages/next/src/server/lib/router-utils/filesystem.ts index cd276cd69eb58b..be19679c7878f7 100644 --- a/packages/next/src/server/lib/router-utils/filesystem.ts +++ b/packages/next/src/server/lib/router-utils/filesystem.ts @@ -31,6 +31,7 @@ import { pathHasPrefix } from '../../../shared/lib/router/utils/path-has-prefix' import { normalizeLocalePath } from '../../../shared/lib/i18n/normalize-locale-path' import { removePathPrefix } from '../../../shared/lib/router/utils/remove-path-prefix' import { getMiddlewareRouteMatcher } from '../../../shared/lib/router/utils/middleware-route-matcher' +import { getDefaultMiddlewareMatcher } from '../../../shared/lib/router/utils/get-default-middleware-matcher' import { APP_PATH_ROUTES_MANIFEST, BUILD_ID_FILE, @@ -314,7 +315,7 @@ export async function setupFsCheck(opts: { } else if (functionsConfigManifest?.functions['/_middleware']) { middlewareMatcher = getMiddlewareRouteMatcher( functionsConfigManifest.functions['/_middleware'].matchers ?? [ - { regexp: '.*', originalSource: '/:path*' }, + getDefaultMiddlewareMatcher(opts.config), ] ) } diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 0df31c045d5a25..c66090d75a19d7 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -48,6 +48,7 @@ import { } from '../../../shared/lib/constants' import { getMiddlewareRouteMatcher } from '../../../shared/lib/router/utils/middleware-route-matcher' +import { getDefaultMiddlewareMatcher } from '../../../shared/lib/router/utils/get-default-middleware-matcher' import { isMiddlewareFile, @@ -472,7 +473,7 @@ async function startWatcher( serverFields.actualMiddlewareFile ) middlewareMatchers = staticInfo.middleware?.matchers || [ - { regexp: '^/.*$', originalSource: '/:path*' }, + getDefaultMiddlewareMatcher(opts.nextConfig), ] continue } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 7e5f627ffb62ca..cdc8731129d2c2 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -71,6 +71,7 @@ import type { LoadComponentsReturnType } from './load-components' import isError, { getProperError } from '../lib/is-error' import { splitCookiesString, toNodeOutgoingHttpHeaders } from './web/utils' import { getMiddlewareRouteMatcher } from '../shared/lib/router/utils/middleware-route-matcher' +import { getDefaultMiddlewareMatcher } from '../shared/lib/router/utils/get-default-middleware-matcher' import { loadEnvConfig } from '@next/env' import { urlQueryToSearchParams } from '../shared/lib/router/utils/querystring' import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash' @@ -1455,13 +1456,13 @@ export default class NextNodeServer extends BaseServer< const middlewareModule = await this.loadNodeMiddleware() if (middlewareModule) { + const matchers = middlewareModule.config?.matchers || [ + getDefaultMiddlewareMatcher(this.nextConfig), + ] return { - match: getMiddlewareRouteMatcher( - middlewareModule.config?.matchers || [ - { regexp: '.*', originalSource: '/:path*' }, - ] - ), + match: getMiddlewareRouteMatcher(matchers), page: '/', + matchers, } } @@ -1471,6 +1472,7 @@ export default class NextNodeServer extends BaseServer< return { match: getMiddlewareMatcher(middleware), page: '/', + matchers: middleware.matchers, } } diff --git a/packages/next/src/shared/lib/router/utils/get-default-middleware-matcher.ts b/packages/next/src/shared/lib/router/utils/get-default-middleware-matcher.ts new file mode 100644 index 00000000000000..3d5c8830b0e27f --- /dev/null +++ b/packages/next/src/shared/lib/router/utils/get-default-middleware-matcher.ts @@ -0,0 +1,21 @@ +import type { NextConfig } from '../../../../types' +import type { MiddlewareMatcher } from '../../../../build/analysis/get-page-static-info' + +export function getDefaultMiddlewareMatcher({ + skipMiddlewareNextInternalRoutes, +}: NextConfig): MiddlewareMatcher { + if (skipMiddlewareNextInternalRoutes !== false) { + // Skip "/_next/" internal routes, except for "/_next/data/" which is needed for + // client-side navigation. Do not consider basePath as the user cannot create a + // route starts with underscore. + return { + regexp: '^(?!.*\\/\\_next\\/(?!data\\/)).*', + originalSource: '/((?!_next/(?!data/))[^]*)*', + } + } + + return { + regexp: '^/.*$', + originalSource: '/:path*', + } +} From 1f7891dedae057930a2e33376b79f9019bdc76e0 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Thu, 25 Sep 2025 16:44:38 +0200 Subject: [PATCH 2/5] Add test --- .../app/layout.tsx | 11 +++++++++++ .../app/page.tsx | 3 +++ ...skip-next-internal-routes-base-path.test.ts | 18 ++++++++++++++++++ .../middleware.ts | 7 +++++++ .../next.config.js | 6 ++++++ .../app/layout.tsx | 11 +++++++++++ .../app/page.tsx | 3 +++ ...e-skip-next-internal-routes-opt-out.test.ts | 18 ++++++++++++++++++ .../middleware.ts | 7 +++++++ .../next.config.js | 6 ++++++ .../app/layout.tsx | 11 +++++++++++ .../app/page.tsx | 3 +++ ...iddleware-skip-next-internal-routes.test.ts | 18 ++++++++++++++++++ .../middleware.ts | 7 +++++++ .../next.config.js | 4 ++++ 15 files changed, 133 insertions(+) create mode 100644 test/e2e/middleware-skip-next-internal-routes-base-path/app/layout.tsx create mode 100644 test/e2e/middleware-skip-next-internal-routes-base-path/app/page.tsx create mode 100644 test/e2e/middleware-skip-next-internal-routes-base-path/middleware-skip-next-internal-routes-base-path.test.ts create mode 100644 test/e2e/middleware-skip-next-internal-routes-base-path/middleware.ts create mode 100644 test/e2e/middleware-skip-next-internal-routes-base-path/next.config.js create mode 100644 test/e2e/middleware-skip-next-internal-routes-opt-out/app/layout.tsx create mode 100644 test/e2e/middleware-skip-next-internal-routes-opt-out/app/page.tsx create mode 100644 test/e2e/middleware-skip-next-internal-routes-opt-out/middleware-skip-next-internal-routes-opt-out.test.ts create mode 100644 test/e2e/middleware-skip-next-internal-routes-opt-out/middleware.ts create mode 100644 test/e2e/middleware-skip-next-internal-routes-opt-out/next.config.js create mode 100644 test/e2e/middleware-skip-next-internal-routes/app/layout.tsx create mode 100644 test/e2e/middleware-skip-next-internal-routes/app/page.tsx create mode 100644 test/e2e/middleware-skip-next-internal-routes/middleware-skip-next-internal-routes.test.ts create mode 100644 test/e2e/middleware-skip-next-internal-routes/middleware.ts create mode 100644 test/e2e/middleware-skip-next-internal-routes/next.config.js diff --git a/test/e2e/middleware-skip-next-internal-routes-base-path/app/layout.tsx b/test/e2e/middleware-skip-next-internal-routes-base-path/app/layout.tsx new file mode 100644 index 00000000000000..08eaa94fdc8896 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-base-path/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/middleware-skip-next-internal-routes-base-path/app/page.tsx b/test/e2e/middleware-skip-next-internal-routes-base-path/app/page.tsx new file mode 100644 index 00000000000000..ff7159d9149fee --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-base-path/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/middleware-skip-next-internal-routes-base-path/middleware-skip-next-internal-routes-base-path.test.ts b/test/e2e/middleware-skip-next-internal-routes-base-path/middleware-skip-next-internal-routes-base-path.test.ts new file mode 100644 index 00000000000000..2fb46031a98bf1 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-base-path/middleware-skip-next-internal-routes-base-path.test.ts @@ -0,0 +1,18 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('middleware skip Next.js internal routes with base path', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should execute middleware on regular routes', async () => { + const res = await next.fetch('/base') + expect(res.status).toBe(200) + expect(res.headers.get('x-middleware-executed')).toBe('true') + }) + + it('should NOT execute middleware on _next routes', async () => { + const res = await next.fetch('/base/_next/static/chunks/webpack.js') + expect(res.headers.get('x-middleware-executed')).toBeFalsy() + }) +}) diff --git a/test/e2e/middleware-skip-next-internal-routes-base-path/middleware.ts b/test/e2e/middleware-skip-next-internal-routes-base-path/middleware.ts new file mode 100644 index 00000000000000..97426e940d1f77 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-base-path/middleware.ts @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server' + +export function middleware() { + const response = NextResponse.next() + response.headers.set('x-middleware-executed', 'true') + return response +} diff --git a/test/e2e/middleware-skip-next-internal-routes-base-path/next.config.js b/test/e2e/middleware-skip-next-internal-routes-base-path/next.config.js new file mode 100644 index 00000000000000..fa1d25f445b248 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-base-path/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + basePath: '/base', +} + +module.exports = nextConfig diff --git a/test/e2e/middleware-skip-next-internal-routes-opt-out/app/layout.tsx b/test/e2e/middleware-skip-next-internal-routes-opt-out/app/layout.tsx new file mode 100644 index 00000000000000..08eaa94fdc8896 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-opt-out/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/middleware-skip-next-internal-routes-opt-out/app/page.tsx b/test/e2e/middleware-skip-next-internal-routes-opt-out/app/page.tsx new file mode 100644 index 00000000000000..ff7159d9149fee --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-opt-out/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/middleware-skip-next-internal-routes-opt-out/middleware-skip-next-internal-routes-opt-out.test.ts b/test/e2e/middleware-skip-next-internal-routes-opt-out/middleware-skip-next-internal-routes-opt-out.test.ts new file mode 100644 index 00000000000000..9e4f81c5117423 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-opt-out/middleware-skip-next-internal-routes-opt-out.test.ts @@ -0,0 +1,18 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('middleware skip Next.js internal routes (opt-out)', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should execute middleware on regular routes', async () => { + const res = await next.fetch('/') + expect(res.status).toBe(200) + expect(res.headers.get('x-middleware-executed')).toBe('true') + }) + + it('should ALSO execute middleware on _next routes when opted out', async () => { + const res = await next.fetch('/_next/static/chunks/webpack.js') + expect(res.headers.get('x-middleware-executed')).toBe('true') + }) +}) diff --git a/test/e2e/middleware-skip-next-internal-routes-opt-out/middleware.ts b/test/e2e/middleware-skip-next-internal-routes-opt-out/middleware.ts new file mode 100644 index 00000000000000..97426e940d1f77 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-opt-out/middleware.ts @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server' + +export function middleware() { + const response = NextResponse.next() + response.headers.set('x-middleware-executed', 'true') + return response +} diff --git a/test/e2e/middleware-skip-next-internal-routes-opt-out/next.config.js b/test/e2e/middleware-skip-next-internal-routes-opt-out/next.config.js new file mode 100644 index 00000000000000..a590bd7b5ce14b --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes-opt-out/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + skipMiddlewareNextInternalRoutes: false, +} + +module.exports = nextConfig diff --git a/test/e2e/middleware-skip-next-internal-routes/app/layout.tsx b/test/e2e/middleware-skip-next-internal-routes/app/layout.tsx new file mode 100644 index 00000000000000..08eaa94fdc8896 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/middleware-skip-next-internal-routes/app/page.tsx b/test/e2e/middleware-skip-next-internal-routes/app/page.tsx new file mode 100644 index 00000000000000..ff7159d9149fee --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/middleware-skip-next-internal-routes/middleware-skip-next-internal-routes.test.ts b/test/e2e/middleware-skip-next-internal-routes/middleware-skip-next-internal-routes.test.ts new file mode 100644 index 00000000000000..9fa90820d5759c --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes/middleware-skip-next-internal-routes.test.ts @@ -0,0 +1,18 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('middleware skip Next.js internal routes', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should execute middleware on regular routes', async () => { + const res = await next.fetch('/') + expect(res.status).toBe(200) + expect(res.headers.get('x-middleware-executed')).toBe('true') + }) + + it('should NOT execute middleware on _next routes', async () => { + const res = await next.fetch('/_next/static/chunks/webpack.js') + expect(res.headers.get('x-middleware-executed')).toBeFalsy() + }) +}) diff --git a/test/e2e/middleware-skip-next-internal-routes/middleware.ts b/test/e2e/middleware-skip-next-internal-routes/middleware.ts new file mode 100644 index 00000000000000..97426e940d1f77 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes/middleware.ts @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server' + +export function middleware() { + const response = NextResponse.next() + response.headers.set('x-middleware-executed', 'true') + return response +} diff --git a/test/e2e/middleware-skip-next-internal-routes/next.config.js b/test/e2e/middleware-skip-next-internal-routes/next.config.js new file mode 100644 index 00000000000000..767719fc4fba59 --- /dev/null +++ b/test/e2e/middleware-skip-next-internal-routes/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +module.exports = nextConfig From 7e8609b7b53d9f41a8e86de8e0742b34df66ae7d Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 7 Oct 2025 22:34:58 +0200 Subject: [PATCH 3/5] Fix existing tests --- test/e2e/middleware-general/test/index.test.ts | 4 +++- test/e2e/middleware-trailing-slash/app/next.config.js | 2 ++ test/e2e/skip-trailing-slash-redirect/app/next.config.js | 2 ++ test/integration/next-image-new/middleware/next.config.js | 4 ++++ 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 test/integration/next-image-new/middleware/next.config.js diff --git a/test/e2e/middleware-general/test/index.test.ts b/test/e2e/middleware-general/test/index.test.ts index 0d616d10b5132d..3285390b43ca2c 100644 --- a/test/e2e/middleware-general/test/index.test.ts +++ b/test/e2e/middleware-general/test/index.test.ts @@ -39,6 +39,8 @@ describe('Middleware Runtime', () => { ), }, nextConfig: { + // This test needs to intercept internal routes /_next/ (skipped by default) + skipMiddlewareNextInternalRoutes: false, experimental: { webpackBuildWorker: true, }, @@ -127,7 +129,7 @@ describe('Middleware Runtime', () => { runtime: 'nodejs', matchers: [ { - regexp: '^.*$', + regexp: '^/.*$', originalSource: '/:path*', }, ], diff --git a/test/e2e/middleware-trailing-slash/app/next.config.js b/test/e2e/middleware-trailing-slash/app/next.config.js index 0c482151c3a261..7bbcc25aa2ac67 100644 --- a/test/e2e/middleware-trailing-slash/app/next.config.js +++ b/test/e2e/middleware-trailing-slash/app/next.config.js @@ -1,4 +1,6 @@ module.exports = { + // This test needs to intercept internal routes /_next/ (skipped by default) + skipMiddlewareNextInternalRoutes: false, trailingSlash: true, redirects() { return [ diff --git a/test/e2e/skip-trailing-slash-redirect/app/next.config.js b/test/e2e/skip-trailing-slash-redirect/app/next.config.js index 5b0be24a8af11d..ec8f1f73a1db3f 100644 --- a/test/e2e/skip-trailing-slash-redirect/app/next.config.js +++ b/test/e2e/skip-trailing-slash-redirect/app/next.config.js @@ -1,5 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + // This test needs to intercept internal routes /_next/ (skipped by default) + skipMiddlewareNextInternalRoutes: false, skipMiddlewareUrlNormalize: true, skipTrailingSlashRedirect: true, experimental: { diff --git a/test/integration/next-image-new/middleware/next.config.js b/test/integration/next-image-new/middleware/next.config.js new file mode 100644 index 00000000000000..613d90fa882e77 --- /dev/null +++ b/test/integration/next-image-new/middleware/next.config.js @@ -0,0 +1,4 @@ +module.exports = { + // This test needs to intercept internal routes /_next/ (skipped by default) + skipMiddlewareNextInternalRoutes: false, +} From b856935f0c4278169a90c3516e8b9fd409cde5fb Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 8 Oct 2025 13:14:26 -0700 Subject: [PATCH 4/5] Apply suggestions from code review --- crates/next-api/src/middleware.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index 696512a4078bd8..4c659310b54b6d 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -55,7 +55,7 @@ fn get_default_middleware_matcher( // route starts with underscore. MiddlewareMatcher { regexp: Some(rcstr!("^(?!.*\\/\\_next\\/(?!data\\/)).*")), - original_source: rcstr!("/:path*"), + original_source: rcstr!("'/((?!_next/(?!data/))[^]*)*'"), ..Default::default() } } else { From a6ad8ae0b698e1b060a0bfb74ffb82fee877df4a Mon Sep 17 00:00:00 2001 From: Jiwon Choi Date: Wed, 8 Oct 2025 22:27:13 +0200 Subject: [PATCH 5/5] Remove single quote from regex Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- crates/next-api/src/middleware.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index 4c659310b54b6d..ace2d5c0b9adc6 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -55,7 +55,7 @@ fn get_default_middleware_matcher( // route starts with underscore. MiddlewareMatcher { regexp: Some(rcstr!("^(?!.*\\/\\_next\\/(?!data\\/)).*")), - original_source: rcstr!("'/((?!_next/(?!data/))[^]*)*'"), + original_source: rcstr!("/((?!_next/(?!data/))[^]*)*"), ..Default::default() } } else {