Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions crates/next-api/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>,
) -> 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!("'/((?!_next/(?!data/))[^]*)*'"),
..Default::default()
}
} else {
MiddlewareMatcher {
regexp: Some(rcstr!("^/.*$")),
original_source: rcstr!("/:path*"),
..Default::default()
}
}
}

#[turbo_tasks::value]
pub struct MiddlewareEndpoint {
project: ResolvedVc<Project>,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub struct NextConfig {
asset_prefix: Option<RcStr>,
base_path: Option<RcStr>,
skip_middleware_url_normalize: Option<bool>,
skip_middleware_next_internal_routes: Option<bool>,
skip_trailing_slash_redirect: Option<bool>,
i18n: Option<I18NConfig>,
cross_origin: Option<CrossOriginConfig>,
Expand Down Expand Up @@ -1343,6 +1344,11 @@ impl NextConfig {
Vc::cell(self.base_path.clone())
}

#[turbo_tasks::function]
pub fn skip_middleware_next_internal_routes(&self) -> Vc<Option<bool>> {
Vc::cell(self.skip_middleware_next_internal_routes)
}

#[turbo_tasks::function]
pub fn cache_handler(&self, project_path: FileSystemPath) -> Result<Vc<OptionFileSystemPath>> {
if let Some(handler) = &self.cache_handler {
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -898,7 +899,7 @@ export async function createEntrypoints(

if (isMiddlewareFile(page)) {
middlewareMatchers = staticInfo.middleware?.matchers ?? [
{ regexp: '.*', originalSource: '/:path*' },
getDefaultMiddlewareMatcher(params.config),
]
}

Expand Down
6 changes: 2 additions & 4 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -2590,10 +2591,7 @@ export default async function build(
functionsConfigManifest.functions['/_middleware'] = {
runtime: staticInfo.runtime,
matchers: staticInfo.middleware?.matchers ?? [
{
regexp: '^.*$',
originalSource: '/:path*',
},
getDefaultMiddlewareMatcher(config),
],
}

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
54 changes: 37 additions & 17 deletions packages/next/src/build/webpack/plugins/middleware-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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[]
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -818,19 +828,28 @@ interface Options {
sriEnabled: boolean
rewrites: CustomRoutes['rewrites']
edgeEnvironments: EdgeRuntimeEnvironments
nextConfig: NextConfigComplete
}

export default class MiddlewarePlugin {
private readonly dev: Options['dev']
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) {
Expand Down Expand Up @@ -886,6 +905,7 @@ export default class MiddlewarePlugin {
rewrites: this.rewrites,
edgeEnvironments: this.edgeEnvironments,
dev: this.dev,
nextConfig: this.nextConfig,
},
})
)
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ export const configSchema: zod.ZodType<NextConfig> = 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(),
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/server/lib/router-utils/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
]
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -472,7 +473,7 @@ async function startWatcher(
serverFields.actualMiddlewareFile
)
middlewareMatchers = staticInfo.middleware?.matchers || [
{ regexp: '^/.*$', originalSource: '/:path*' },
getDefaultMiddlewareMatcher(opts.nextConfig),
]
continue
}
Expand Down
12 changes: 7 additions & 5 deletions packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
}
}

Expand All @@ -1471,6 +1472,7 @@ export default class NextNodeServer extends BaseServer<
return {
match: getMiddlewareMatcher(middleware),
page: '/',
matchers: middleware.matchers,
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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*',
}
}
4 changes: 3 additions & 1 deletion test/e2e/middleware-general/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ describe('Middleware Runtime', () => {
),
},
nextConfig: {
// This test needs to intercept internal routes /_next/ (skipped by default)
skipMiddlewareNextInternalRoutes: false,
experimental: {
webpackBuildWorker: true,
},
Expand Down Expand Up @@ -127,7 +129,7 @@ describe('Middleware Runtime', () => {
runtime: 'nodejs',
matchers: [
{
regexp: '^.*$',
regexp: '^/.*$',
originalSource: '/:path*',
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -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()
})
})
Loading
Loading