From d74b2cc534bcfda7d4bf6104c36167ab5b0b3095 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:56:02 -0700 Subject: [PATCH 01/20] Only allow edge runtime in proxy --- crates/next-api/src/middleware.rs | 20 ++++++-- crates/next-core/src/middleware.rs | 2 +- crates/next-core/src/segment_config.rs | 46 +++++++++++++------ .../build/analysis/get-page-static-info.ts | 12 +++++ .../next/src/build/templates/middleware.ts | 28 +---------- packages/next/src/build/utils.ts | 8 ++++ test/e2e/app-dir/proxy-runtime/app/layout.tsx | 8 ++++ test/e2e/app-dir/proxy-runtime/app/page.tsx | 3 ++ test/e2e/app-dir/proxy-runtime/next.config.js | 6 +++ .../proxy-runtime/proxy-runtime.test.ts | 43 +++++++++++++++++ test/e2e/app-dir/proxy-runtime/proxy.ts | 3 ++ 11 files changed, 132 insertions(+), 47 deletions(-) create mode 100644 test/e2e/app-dir/proxy-runtime/app/layout.tsx create mode 100644 test/e2e/app-dir/proxy-runtime/app/page.tsx create mode 100644 test/e2e/app-dir/proxy-runtime/next.config.js create mode 100644 test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts create mode 100644 test/e2e/app-dir/proxy-runtime/proxy.ts diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index be41bce9a4884..be1cd6e4b1388 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -81,20 +81,32 @@ impl MiddlewareEndpoint { ) .module(); + let userland_path = userland_module.ident().path().await?; + let is_proxy = userland_path.file_stem() == Some("proxy"); + let module = get_middleware_module( *self.asset_context, self.project.project_path().owned().await?, userland_module, + is_proxy, ); - let runtime = parse_segment_config_from_source(*self.source, ParseSegmentMode::Base) - .await? - .runtime - .unwrap_or(NextRuntime::Edge); + let runtime = parse_segment_config_from_source( + *self.source, + if is_proxy { + ParseSegmentMode::Proxy + } else { + ParseSegmentMode::Base + }, + ) + .await? + .runtime + .unwrap_or(NextRuntime::NodeJs); if matches!(runtime, NextRuntime::NodeJs) { return Ok(module); } + Ok(wrap_edge_entry( *self.asset_context, self.project.project_path().owned().await?, diff --git a/crates/next-core/src/middleware.rs b/crates/next-core/src/middleware.rs index 56845fd72accf..e2a3d7b7c8d47 100644 --- a/crates/next-core/src/middleware.rs +++ b/crates/next-core/src/middleware.rs @@ -32,12 +32,12 @@ pub async fn get_middleware_module( asset_context: Vc>, project_root: FileSystemPath, userland_module: ResolvedVc>, + is_proxy: bool, ) -> Result>> { const INNER: &str = "INNER_MIDDLEWARE_MODULE"; // Determine if this is a proxy file by checking the module path let userland_path = userland_module.ident().path().await?; - let is_proxy = userland_path.file_stem() == Some("proxy"); let (file_type, function_name, page_path) = if is_proxy { ("Proxy", "proxy", "/proxy") } else { diff --git a/crates/next-core/src/segment_config.rs b/crates/next-core/src/segment_config.rs index 9baff06482b1c..227012f894a16 100644 --- a/crates/next-core/src/segment_config.rs +++ b/crates/next-core/src/segment_config.rs @@ -289,6 +289,8 @@ pub enum ParseSegmentMode { Base, // Disallows "use client + generateStatic" and ignores/warns about `export const config` App, + // Disallows config = { runtime: "edge" } + Proxy, } /// Parse the raw source code of a file to get the segment config local to that file. @@ -667,21 +669,35 @@ async fn parse_config_value( .await; }; - config.runtime = - match serde_json::from_value(Value::String(val.to_string())) { - Ok(runtime) => Some(runtime), - Err(err) => { - return invalid_config( - source, - "config", - span, - format!("`runtime` has an invalid value: {err}.").into(), - Some(value), - IssueSeverity::Error, - ) - .await; - } - }; + let runtime = match serde_json::from_value(Value::String(val.to_string())) { + Ok(runtime) => Some(runtime), + Err(err) => { + return invalid_config( + source, + "config", + span, + format!("`runtime` has an invalid value: {err}.").into(), + Some(value), + IssueSeverity::Error, + ) + .await; + } + }; + + if mode == ParseSegmentMode::Proxy && runtime == Some(NextRuntime::Edge) { + invalid_config( + source, + "config", + span, + rcstr!("Proxy does not support Edge runtime."), + Some(value), + IssueSeverity::Error, + ) + .await?; + continue; + } + + config.runtime = runtime } "matcher" => { config.middleware_matcher = diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 01314d1449e2e..6b8531b3214f2 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -756,6 +756,18 @@ export async function getPagesPageStaticInfo({ warnAboutExperimentalEdge(isAnAPIRoute ? page! : null) } + if ( + (page === `/${PROXY_FILENAME}` || page === `/src/${PROXY_FILENAME}`) && + isEdgeRuntime(resolvedRuntime) + ) { + const message = `Proxy does not support Edge runtime.` + if (isDev) { + Log.error(message) + } else { + throw new Error(message) + } + } + if (resolvedRuntime === SERVER_RUNTIME.edge && page && !isAnAPIRoute) { const message = `Page ${page} provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.` if (isDev) { diff --git a/packages/next/src/build/templates/middleware.ts b/packages/next/src/build/templates/middleware.ts index 2637bd64dffaa..fa78a1f64ca11 100644 --- a/packages/next/src/build/templates/middleware.ts +++ b/packages/next/src/build/templates/middleware.ts @@ -11,36 +11,10 @@ import { isNextRouterError } from '../../client/components/is-next-router-error' const mod = { ..._mod } -const page = 'VAR_DEFINITION_PAGE' -// Turbopack does not add a `./` prefix to the relative file path, but Webpack does. -const relativeFilePath = 'VAR_MODULE_RELATIVE_PATH' -// @ts-expect-error `page` will be replaced during build +const page: string = 'VAR_DEFINITION_PAGE' const isProxy = page === '/proxy' || page === '/src/proxy' const handler = (isProxy ? mod.proxy : mod.middleware) || mod.default -if (typeof handler !== 'function') { - const fileName = isProxy ? 'proxy' : 'middleware' - // Webpack starts the path with "." as relative, but Turbopack does not. - const resolvedRelativeFilePath = relativeFilePath.startsWith('.') - ? relativeFilePath - : `./${relativeFilePath}` - - throw new Error( - `The file "${resolvedRelativeFilePath}" must export a function, either as a default export or as a named "${fileName}" export.\n` + - `This function is what Next.js runs for every request handled by this ${fileName === 'proxy' ? 'proxy (previously called middleware)' : 'middleware'}.\n\n` + - `Why this happens:\n` + - (isProxy - ? "- You are migrating from `middleware` to `proxy`, but haven't updated the exported function.\n" - : '') + - `- The file exists but doesn't export a function.\n` + - `- The export is not a function (e.g., an object or constant).\n` + - `- There's a syntax error preventing the export from being recognized.\n\n` + - `To fix it:\n` + - `- Ensure this file has either a default or "${fileName}" function export.\n\n` + - `Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` - ) -} - // Proxy will only sent out the FetchEvent to next server, // so load instrumentation module here and track the error inside proxy module. function errorHandledHandler(fn: AdapterOptions['handler']) { diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 78fedfee3a1ff..4e3a3ca716d5c 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -105,6 +105,14 @@ export function isMiddlewareFilename(file?: string | null) { ) } +export function isMiddlewareOnlyFilename(file?: string | null) { + return file === MIDDLEWARE_FILENAME || file === `src/${MIDDLEWARE_FILENAME}` +} + +export function isProxyFilename(file?: string | null) { + return file === PROXY_FILENAME || file === `src/${PROXY_FILENAME}` +} + export function isInstrumentationHookFilename(file?: string | null) { return ( file === INSTRUMENTATION_HOOK_FILENAME || diff --git a/test/e2e/app-dir/proxy-runtime/app/layout.tsx b/test/e2e/app-dir/proxy-runtime/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/proxy-runtime/app/page.tsx b/test/e2e/app-dir/proxy-runtime/app/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/proxy-runtime/next.config.js b/test/e2e/app-dir/proxy-runtime/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts new file mode 100644 index 0000000000000..93075655a29de --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts @@ -0,0 +1,43 @@ +import { nextTestSetup } from 'e2e-utils' +import stripAnsi from 'strip-ansi' + +describe('proxy-missing-export', () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + skipStart: true, + }) + + if (skipped) { + return + } + + it('should error when proxy file has invalid export named middleware', async () => { + let cliOutput: string + + if (isNextDev) { + await next.start().catch(() => {}) + // Use .catch() because Turbopack errors during compile and exits before runtime. + await next.browser('/').catch(() => {}) + cliOutput = next.cliOutput + } else { + cliOutput = (await next.build()).cliOutput + } + + if (process.env.IS_TURBOPACK_TEST) { + expect(stripAnsi(cliOutput)).toContain(`proxy.ts:3:14 +Next.js can't recognize the exported \`config\` field in route. Proxy does not support Edge runtime. + 1 | export default function () {} + 2 | +> 3 | export const config = { runtime: 'edge' } + | ^^^^^^ + 4 | + +The exported configuration object in a source file needs to have a very specific format from which some properties can be statically parsed at compiled-time.`) + } else { + expect(cliOutput).toContain(`Proxy does not support Edge runtime.`) + } + + await next.stop() + }) +}) diff --git a/test/e2e/app-dir/proxy-runtime/proxy.ts b/test/e2e/app-dir/proxy-runtime/proxy.ts new file mode 100644 index 0000000000000..586ed05ca9d43 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/proxy.ts @@ -0,0 +1,3 @@ +export default function () {} + +export const config = { runtime: 'edge' } From 110e473e9df46faf7a96b7421e61fbe71a7a859d Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:15:54 -0700 Subject: [PATCH 02/20] fixup --- crates/next-core/src/middleware.rs | 6 +----- packages/next/src/build/entries.ts | 6 ++++++ .../src/build/webpack/loaders/next-middleware-loader.ts | 3 --- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/next-core/src/middleware.rs b/crates/next-core/src/middleware.rs index e2a3d7b7c8d47..51cd7c290caae 100644 --- a/crates/next-core/src/middleware.rs +++ b/crates/next-core/src/middleware.rs @@ -91,11 +91,7 @@ pub async fn get_middleware_module( let source = load_next_js_template( "middleware.js", project_root, - &[ - ("VAR_USERLAND", INNER), - ("VAR_DEFINITION_PAGE", page_path), - ("VAR_MODULE_RELATIVE_PATH", userland_path.path.as_str()), - ], + &[("VAR_USERLAND", INNER), ("VAR_DEFINITION_PAGE", page_path)], &[], &[], ) diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 50a4762661e5c..5f82ab991050c 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -45,6 +45,7 @@ import { isMiddlewareFilename, isInstrumentationHookFile, isInstrumentationHookFilename, + isProxyFilename, } from './utils' import { getPageStaticInfo } from './analysis/get-page-static-info' import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' @@ -764,6 +765,11 @@ export function runDependingOnPageType(params: { return } + if (isProxyFilename(params.page)) { + params.onServer() + return + } + if (isMiddlewareFile(params.page)) { if (params.pageRuntime === 'nodejs') { params.onServer() diff --git a/packages/next/src/build/webpack/loaders/next-middleware-loader.ts b/packages/next/src/build/webpack/loaders/next-middleware-loader.ts index 512d1ca32d05c..9f2066f3a4c29 100644 --- a/packages/next/src/build/webpack/loaders/next-middleware-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-middleware-loader.ts @@ -70,8 +70,5 @@ export default async function middlewareLoader(this: any) { return await loadEntrypoint('middleware', { VAR_USERLAND: pagePath, VAR_DEFINITION_PAGE: page, - // Turbopack sets `VAR_USERLAND` to `INNER_MIDDLEWARE_MODULE`, so use - // `VAR_MODULE_RELATIVE_PATH` for error messages. - VAR_MODULE_RELATIVE_PATH: pagePath, }) } From 7c3585b3f697a09dea8ea72369418d6948e74ef9 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:47:56 -0700 Subject: [PATCH 03/20] turbopack --- crates/next-api/src/middleware.rs | 30 +++++++-------------- crates/next-api/src/project.rs | 44 +++++++++++++++---------------- 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index be1cd6e4b1388..97b2a9438f6b4 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -6,8 +6,7 @@ use next_core::{ middleware::get_middleware_module, next_edge::entry::wrap_edge_entry, next_manifests::{EdgeFunctionDefinition, MiddlewaresManifestV2, ProxyMatcher, Regions}, - parse_segment_config_from_source, - segment_config::ParseSegmentMode, + segment_config::NextSegmentConfig, util::{MiddlewareMatcherKind, NextRuntime}, }; use tracing::Instrument; @@ -49,6 +48,8 @@ pub struct MiddlewareEndpoint { source: ResolvedVc>, app_dir: Option, ecmascript_client_reference_transition_name: Option, + config: ResolvedVc, + runtime: NextRuntime, } #[turbo_tasks::value_impl] @@ -60,6 +61,8 @@ impl MiddlewareEndpoint { source: ResolvedVc>, app_dir: Option, ecmascript_client_reference_transition_name: Option, + config: ResolvedVc, + runtime: NextRuntime, ) -> Vc { Self { project, @@ -67,6 +70,8 @@ impl MiddlewareEndpoint { source, app_dir, ecmascript_client_reference_transition_name, + config, + runtime, } .cell() } @@ -91,19 +96,7 @@ impl MiddlewareEndpoint { is_proxy, ); - let runtime = parse_segment_config_from_source( - *self.source, - if is_proxy { - ParseSegmentMode::Proxy - } else { - ParseSegmentMode::Base - }, - ) - .await? - .runtime - .unwrap_or(NextRuntime::NodeJs); - - if matches!(runtime, NextRuntime::NodeJs) { + if matches!(self.runtime, NextRuntime::NodeJs) { return Ok(module); } @@ -164,10 +157,7 @@ impl MiddlewareEndpoint { #[turbo_tasks::function] async fn output_assets(self: Vc) -> Result> { let this = self.await?; - - let config = - parse_segment_config_from_source(*self.await?.source, ParseSegmentMode::Base).await?; - let runtime = config.runtime.unwrap_or(NextRuntime::Edge); + let config = this.config.await?; let next_config = this.project.next_config(); let i18n = next_config.i18n().await?; @@ -235,7 +225,7 @@ impl MiddlewareEndpoint { }] }; - if matches!(runtime, NextRuntime::NodeJs) { + if matches!(this.runtime, NextRuntime::NodeJs) { let chunk = self.node_chunk().to_resolved().await?; let mut output_assets = vec![chunk]; if this.project.next_mode().await?.is_production() { diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 9193d6d288738..5258e947d6dde 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -1437,28 +1437,6 @@ impl Project { ))) } - #[turbo_tasks::function] - async fn middleware_context(self: Vc) -> Result>> { - let edge_module_context = self.edge_middleware_context(); - - let middleware = self.find_middleware(); - let FindContextFileResult::Found(fs_path, _) = &*middleware.await? else { - return Ok(edge_module_context); - }; - let source = Vc::upcast(FileSource::new(fs_path.clone())); - - let runtime = parse_segment_config_from_source(source, ParseSegmentMode::Base) - .await? - .runtime - .unwrap_or(NextRuntime::Edge); - - if matches!(runtime, NextRuntime::NodeJs) { - Ok(self.node_middleware_context()) - } else { - Ok(edge_module_context) - } - } - #[turbo_tasks::function] async fn find_middleware(self: Vc) -> Result> { Ok(find_context_file( @@ -1483,7 +1461,25 @@ impl Project { .as_ref() .map(|_| AppProject::client_transition_name()); - let middleware_asset_context = self.middleware_context(); + let is_proxy = fs_path.file_stem() == Some("proxy"); + let config = parse_segment_config_from_source( + source, + if is_proxy { + ParseSegmentMode::Proxy + } else { + ParseSegmentMode::Base + }, + ); + let runtime = config.await?.runtime.unwrap_or(if is_proxy { + NextRuntime::NodeJs + } else { + NextRuntime::Edge + }); + + let middleware_asset_context = match runtime { + NextRuntime::NodeJs => self.node_middleware_context(), + NextRuntime::Edge => self.edge_middleware_context(), + }; Ok(Vc::upcast(MiddlewareEndpoint::new( self, @@ -1491,6 +1487,8 @@ impl Project { source, app_dir.clone(), ecmascript_client_reference_transition_name, + config, + runtime, ))) } From 9b8c455d20b0827cddef2265feaf7af83402ea47 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:48:40 -0700 Subject: [PATCH 04/20] override webpack --- packages/next/src/build/analysis/get-page-static-info.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 6b8531b3214f2..332ca58976492 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -750,7 +750,7 @@ export async function getPagesPageStaticInfo({ const config = parsePagesSegmentConfig(exportedConfig, route) const isAnAPIRoute = isAPIRoute(route) - const resolvedRuntime = config.runtime ?? config.config?.runtime + let resolvedRuntime = config.runtime ?? config.config?.runtime if (resolvedRuntime === SERVER_RUNTIME.experimentalEdge) { warnAboutExperimentalEdge(isAnAPIRoute ? page! : null) @@ -760,6 +760,7 @@ export async function getPagesPageStaticInfo({ (page === `/${PROXY_FILENAME}` || page === `/src/${PROXY_FILENAME}`) && isEdgeRuntime(resolvedRuntime) ) { + resolvedRuntime = SERVER_RUNTIME.nodejs const message = `Proxy does not support Edge runtime.` if (isDev) { Log.error(message) From 36d287cfa820811d1a168b94ccc454694790aeaa Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:51:56 -0700 Subject: [PATCH 05/20] print top level issues in handleProjectUpdates --- .../src/server/dev/hot-reloader-turbopack.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index caa6ddbc3c5ca..d9c027e143cd4 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -89,6 +89,7 @@ import { processIssues, renderStyledStringToErrorAnsi, type EntryIssuesMap, + type IssuesMap, type TopLevelIssuesMap, } from '../../shared/lib/turbopack/utils' import { getDevOverlayFontMiddleware } from '../../next-devtools/server/font/get-dev-overlay-font-middleware' @@ -1432,28 +1433,36 @@ export async function createHotReloaderTurbopack( case 'end': { sendEnqueuedMessages() + function addToErrorsMap( + errorsMap: Map, + issueMap: IssuesMap + ) { + for (const [key, issue] of issueMap) { + if (issue.severity === 'warning') continue + if (errorsMap.has(key)) continue + + const message = formatIssue(issue) + + errorsMap.set(key, { + message, + details: issue.detail + ? renderStyledStringToErrorAnsi(issue.detail) + : undefined, + }) + } + } + function addErrors( errorsMap: Map, issues: EntryIssuesMap ) { for (const issueMap of issues.values()) { - for (const [key, issue] of issueMap) { - if (issue.severity === 'warning') continue - if (errorsMap.has(key)) continue - - const message = formatIssue(issue) - - errorsMap.set(key, { - message, - details: issue.detail - ? renderStyledStringToErrorAnsi(issue.detail) - : undefined, - }) - } + addToErrorsMap(errorsMap, issueMap) } } const errors = new Map() + addToErrorsMap(errors, currentTopLevelIssues) addErrors(errors, currentEntryIssues) for (const client of [ From 4479cd23fd642201375f955e5c8c5d40bed36ac4 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 20 Oct 2025 22:48:47 -0700 Subject: [PATCH 06/20] Handle for Webpack --- .../build/analysis/get-page-static-info.ts | 33 ++++++++++------ packages/next/src/build/entries.ts | 6 +-- packages/next/src/build/index.ts | 24 ++++++------ packages/next/src/build/output/log.ts | 9 +++++ packages/next/src/build/utils.ts | 12 ++---- packages/next/src/server/next-server.ts | 38 +++++++++++++++---- .../proxy-runtime-nodejs.test.ts | 12 ++++++ .../e2e/app-dir/proxy-runtime-nodejs/proxy.ts | 11 ++++++ .../proxy-runtime/proxy-runtime.test.ts | 4 +- 9 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 test/e2e/app-dir/proxy-runtime-nodejs/proxy-runtime-nodejs.test.ts create mode 100644 test/e2e/app-dir/proxy-runtime-nodejs/proxy.ts diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 332ca58976492..a7974ba67f16e 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -40,6 +40,7 @@ import { } from '../segment-config/middleware/middleware-config' import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path' +import { isProxyFile } from '../utils' const PARSE_PATTERN = /(?(params: { return } - if (isProxyFilename(params.page)) { + if (isProxyFile(params.page)) { params.onServer() return } - if (isMiddlewareFile(params.page)) { + if (isMiddlewareFile(params.page) && !isProxyFile(params.page)) { if (params.pageRuntime === 'nodejs') { params.onServer() return diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index bb52c5db5984f..b52d2c1959632 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -139,6 +139,7 @@ import { isAppBuiltinPage, collectRoutesUsingEdgeRuntime, collectMeta, + isProxyFile, } from './utils' import type { PageInfo, PageInfos } from './utils' import type { FallbackRouteParam, PrerenderedRoute } from './static-paths/types' @@ -1214,13 +1215,7 @@ export default async function build( for (const rootPath of rootPaths) { const { name: fileBaseName, dir: fileDir } = path.parse(rootPath) - const isAtConventionLevel = - fileDir === '/' || - fileDir === '/src' || - // rootPaths are currently relative paths from the root directory. - // Add safety check here for unexpected future changes. - fileDir === dir || - fileDir === path.join(dir, 'src') + const isAtConventionLevel = fileDir === '/' || fileDir === '/src' if (isAtConventionLevel && fileBaseName === MIDDLEWARE_FILENAME) { middlewareFilePath = rootPath @@ -2615,12 +2610,15 @@ export default async function build( return serverFilesManifest }) - const middlewareFile = rootPaths.find( - (p) => p.includes(MIDDLEWARE_FILENAME) || p.includes(PROXY_FILENAME) - ) + const middlewareFile = proxyFilePath || middlewareFilePath let hasNodeMiddleware = false if (middlewareFile) { + // Is format of `(/src)/(proxy|middleware).`, so split by + // "." and get the first part, regard rest of the extensions + // to match the `page` value format. + const page = middlewareFile.split('.')[0] + const staticInfo = await getStaticInfoIncludingLayouts({ isInsideAppDir: false, pageFilePath: path.join(dir, middlewareFile), @@ -2628,17 +2626,17 @@ export default async function build( appDir, pageExtensions: config.pageExtensions, isDev: false, - page: 'middleware', + page, }) if (staticInfo.hadUnsupportedValue) { errorFromUnsupportedSegmentConfig() } - if (staticInfo.runtime === 'nodejs') { + if (staticInfo.runtime === 'nodejs' || isProxyFile(page)) { hasNodeMiddleware = true functionsConfigManifest.functions['/_middleware'] = { - runtime: staticInfo.runtime, + runtime: 'nodejs', matchers: staticInfo.middleware?.matchers ?? [ { regexp: '^.*$', diff --git a/packages/next/src/build/output/log.ts b/packages/next/src/build/output/log.ts index b62c42316dcc6..41ca0dc254381 100644 --- a/packages/next/src/build/output/log.ts +++ b/packages/next/src/build/output/log.ts @@ -85,3 +85,12 @@ export function warnOnce(...message: any[]) { warn(...message) } } + +const errorOnceCache = new LRUCache(10_000, (value) => value.length) +export function errorOnce(...message: any[]) { + const key = message.join(' ') + if (!errorOnceCache.has(key)) { + errorOnceCache.set(key, key) + error(...message) + } +} diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 4e3a3ca716d5c..01454bb2ffb61 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -105,14 +105,6 @@ export function isMiddlewareFilename(file?: string | null) { ) } -export function isMiddlewareOnlyFilename(file?: string | null) { - return file === MIDDLEWARE_FILENAME || file === `src/${MIDDLEWARE_FILENAME}` -} - -export function isProxyFilename(file?: string | null) { - return file === PROXY_FILENAME || file === `src/${PROXY_FILENAME}` -} - export function isInstrumentationHookFilename(file?: string | null) { return ( file === INSTRUMENTATION_HOOK_FILENAME || @@ -1429,6 +1421,10 @@ export function isMiddlewareFile(file: string) { ) } +export function isProxyFile(file: string) { + return file === `/${PROXY_FILENAME}` || file === `/src/${PROXY_FILENAME}` +} + export function isInstrumentationHookFile(file: string) { return ( file === `/${INSTRUMENTATION_HOOK_FILENAME}` || diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 3e0adebee8842..f5f16c3d266ad 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1574,13 +1574,34 @@ export default class NextNodeServer extends BaseServer< functionsConfig?.functions?.['/_middleware'] ) { // if used with top level await, this will be a promise - return require( - join( - /* turbopackIgnore: true */ this.distDir, - 'server', - 'middleware.js' + // Try loading middleware.js first, then proxy.js. Instead + // of mapping proxy to middleware as the entry, just fallback + // to proxy. + // TODO: Remove this once we handle as the single entrypoint. + try { + return require( + join( + /* turbopackIgnore: true */ this.distDir, + 'server', + 'middleware.js' + ) ) - ) + } catch (middlewareErr) { + if ( + isError(middlewareErr) && + (middlewareErr.code === 'ENOENT' || + middlewareErr.code === 'MODULE_NOT_FOUND') + ) { + return require( + join( + /* turbopackIgnore: true */ this.distDir, + 'server', + 'proxy.js' + ) + ) + } + throw middlewareErr + } } } catch (err) { if ( @@ -1736,7 +1757,10 @@ export default class NextNodeServer extends BaseServer< try { result = await adapterFn({ - handler: middlewareModule.middleware || middlewareModule, + handler: + middlewareModule.proxy || + middlewareModule.middleware || + middlewareModule, request: { ...requestData, body: hasRequestBody diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/proxy-runtime-nodejs.test.ts b/test/e2e/app-dir/proxy-runtime-nodejs/proxy-runtime-nodejs.test.ts new file mode 100644 index 0000000000000..f68f7377ecb75 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/proxy-runtime-nodejs.test.ts @@ -0,0 +1,12 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('proxy-runtime-nodejs', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should use nodejs runtime for proxy by default', async () => { + const browser = await next.browser('/foo') + expect(await browser.elementByCss('p').text()).toBe('hello world') + }) +}) diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/proxy.ts b/test/e2e/app-dir/proxy-runtime-nodejs/proxy.ts new file mode 100644 index 0000000000000..dd49dfb801a3d --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/proxy.ts @@ -0,0 +1,11 @@ +import { NextRequest, NextResponse } from 'next/server' +// Will not work in edge runtime +import { join } from 'path/posix' + +export function proxy(request: NextRequest) { + if (request.nextUrl.pathname === join('/', 'foo')) { + return NextResponse.redirect(new URL('/', request.url)) + } + + return NextResponse.next() +} diff --git a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts index 93075655a29de..36478aca1186c 100644 --- a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts +++ b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts @@ -35,7 +35,9 @@ Next.js can't recognize the exported \`config\` field in route. Proxy does not s The exported configuration object in a source file needs to have a very specific format from which some properties can be statically parsed at compiled-time.`) } else { - expect(cliOutput).toContain(`Proxy does not support Edge runtime.`) + expect(cliOutput).toContain( + `Route segment config is not allowed in Proxy file at "./proxy.ts". Proxy always runs on Node.js runtime. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` + ) } await next.stop() From bc055509370a59567ae9bf9a6697cb87a4973c33 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Oct 2025 00:54:17 -0700 Subject: [PATCH 07/20] Add nodejs module test --- test/e2e/app-dir/proxy-runtime-nodejs/app/layout.tsx | 8 ++++++++ test/e2e/app-dir/proxy-runtime-nodejs/app/page.tsx | 3 +++ test/e2e/app-dir/proxy-runtime-nodejs/next.config.js | 6 ++++++ test/e2e/app-dir/proxy-runtime-nodejs/redirect-path.txt | 1 + 4 files changed, 18 insertions(+) create mode 100644 test/e2e/app-dir/proxy-runtime-nodejs/app/layout.tsx create mode 100644 test/e2e/app-dir/proxy-runtime-nodejs/app/page.tsx create mode 100644 test/e2e/app-dir/proxy-runtime-nodejs/next.config.js create mode 100644 test/e2e/app-dir/proxy-runtime-nodejs/redirect-path.txt diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/app/layout.tsx b/test/e2e/app-dir/proxy-runtime-nodejs/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/app/page.tsx b/test/e2e/app-dir/proxy-runtime-nodejs/app/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/next.config.js b/test/e2e/app-dir/proxy-runtime-nodejs/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/redirect-path.txt b/test/e2e/app-dir/proxy-runtime-nodejs/redirect-path.txt new file mode 100644 index 0000000000000..7370610cce702 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/redirect-path.txt @@ -0,0 +1 @@ +/foo \ No newline at end of file From 6f0e9530801dbc447f30e53f2da5299e2d12f62c Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Oct 2025 07:15:46 -0700 Subject: [PATCH 08/20] Remove node_env gate --- .../build/analysis/get-page-static-info.ts | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index a7974ba67f16e..54a07c1c62acd 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -306,16 +306,13 @@ function validateMiddlewareProxyExports({ ast, page, pageFilePath, + isDev, }: { ast: any page: string pageFilePath: string + isDev: boolean }): void { - // Only validate in build. In development, it will error at runtime. - if (process.env.NODE_ENV !== 'production') { - return - } - // Check if this is middleware/proxy const isMiddleware = page === `/${MIDDLEWARE_FILENAME}` || page === `/src/${MIDDLEWARE_FILENAME}` @@ -397,23 +394,31 @@ function validateMiddlewareProxyExports({ (isMiddleware && hasMiddlewareExport) || (isProxy && hasProxyExport) - const relativeFilePath = `./${relative(process.cwd(), pageFilePath)}` + const relativePath = relative(process.cwd(), pageFilePath) + const resolvedPath = relativePath.startsWith('.') + ? relativePath + : `./${relativePath}` if (!hasValidExport) { - throw new Error( - `The file "${relativeFilePath}" must export a function, either as a default export or as a named "${fileName}" export.\n` + - `This function is what Next.js runs for every request handled by this ${fileName === 'proxy' ? 'proxy (previously called middleware)' : 'middleware'}.\n\n` + - `Why this happens:\n` + - (isProxy - ? "- You are migrating from `middleware` to `proxy`, but haven't updated the exported function.\n" - : '') + - `- The file exists but doesn't export a function.\n` + - `- The export is not a function (e.g., an object or constant).\n` + - `- There's a syntax error preventing the export from being recognized.\n\n` + - `To fix it:\n` + - `- Ensure this file has either a default or "${fileName}" function export.\n\n` + - `Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` - ) + const message = + `The file "${resolvedPath}" must export a function, either as a default export or as a named "${fileName}" export.\n` + + `This function is what Next.js runs for every request handled by this ${fileName === 'proxy' ? 'proxy (previously called middleware)' : 'middleware'}.\n\n` + + `Why this happens:\n` + + (isProxy + ? "- You are migrating from `middleware` to `proxy`, but haven't updated the exported function.\n" + : '') + + `- The file exists but doesn't export a function.\n` + + `- The export is not a function (e.g., an object or constant).\n` + + `- There's a syntax error preventing the export from being recognized.\n\n` + + `To fix it:\n` + + `- Ensure this file has either a default or "${fileName}" function export.\n\n` + + `Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` + + if (isDev) { + Log.errorOnce(message) + } else { + throw new Error(message) + } } } @@ -618,7 +623,12 @@ export async function getAppPageStaticInfo({ } const ast = await parseModule(pageFilePath, content) - validateMiddlewareProxyExports({ ast, page, pageFilePath }) + validateMiddlewareProxyExports({ + ast, + page, + pageFilePath, + isDev: isDev ?? process.env.NODE_ENV === 'development', + }) const { generateStaticParams, @@ -714,7 +724,12 @@ export async function getPagesPageStaticInfo({ } const ast = await parseModule(pageFilePath, content) - validateMiddlewareProxyExports({ ast, page, pageFilePath }) + validateMiddlewareProxyExports({ + ast, + page, + pageFilePath, + isDev: isDev ?? process.env.NODE_ENV === 'development', + }) const { getServerSideProps, getStaticProps, exports } = checkExports( ast, From af19b91a8ab68e22f43da997f637c4ee19ec05a3 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Oct 2025 07:48:04 -0700 Subject: [PATCH 09/20] test: nodejs runtime cannot set draftMode --- .../app-middleware-proxy/app-middleware-proxy.test.ts | 4 +++- test/e2e/app-dir/app-middleware-proxy/proxy.js | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/test/e2e/app-dir/app-middleware-proxy/app-middleware-proxy.test.ts b/test/e2e/app-dir/app-middleware-proxy/app-middleware-proxy.test.ts index b0c080aadbe9a..35c19a0f4e0e8 100644 --- a/test/e2e/app-dir/app-middleware-proxy/app-middleware-proxy.test.ts +++ b/test/e2e/app-dir/app-middleware-proxy/app-middleware-proxy.test.ts @@ -117,7 +117,9 @@ describe('app-dir with proxy', () => { expect(res.headers.get('x-middleware-request-x-from-client3')).toBeNull() }) - it(`Supports draft mode`, async () => { + // Cannot set draftMode in nodejs runtime + // TODO: Investigate https://github.com/vercel/next.js/pull/85174 + it.skip(`Supports draft mode`, async () => { const res = await next.fetch(`${path}?draft=true`) const headers: string = res.headers.get('set-cookie') || '' const bypassCookie = headers diff --git a/test/e2e/app-dir/app-middleware-proxy/proxy.js b/test/e2e/app-dir/app-middleware-proxy/proxy.js index d5a7c82b381ad..58ab998be7186 100644 --- a/test/e2e/app-dir/app-middleware-proxy/proxy.js +++ b/test/e2e/app-dir/app-middleware-proxy/proxy.js @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' -import { headers as nextHeaders, draftMode } from 'next/headers' +import { headers as nextHeaders } from 'next/headers' /** * @param {import('next/server').NextRequest} request @@ -20,9 +20,11 @@ export async function proxy(request) { throw new Error('Expected headers from client to match') } - if (request.nextUrl.searchParams.get('draft')) { - ;(await draftMode()).enable() - } + // Cannot set draftMode in nodejs runtime + // TODO: Investigate https://github.com/vercel/next.js/pull/85174 + // if (request.nextUrl.searchParams.get('draft')) { + // ;(await draftMode()).enable() + // } const removeHeaders = request.nextUrl.searchParams.get('remove-headers') if (removeHeaders) { From b187b620f4e8c12ef630f4dce6b7910801115bdc Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Oct 2025 07:50:15 -0700 Subject: [PATCH 10/20] test: skip for dev-turbo proxy-runtime --- test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts index 36478aca1186c..a45e34ae6fcc0 100644 --- a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts +++ b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts @@ -25,7 +25,10 @@ describe('proxy-missing-export', () => { } if (process.env.IS_TURBOPACK_TEST) { - expect(stripAnsi(cliOutput)).toContain(`proxy.ts:3:14 + if (isNextDev) { + // TODO: Investigate why in dev-turbo, the error is shown in the browser console, not CLI output. + } else { + expect(stripAnsi(cliOutput)).toContain(`proxy.ts:3:14 Next.js can't recognize the exported \`config\` field in route. Proxy does not support Edge runtime. 1 | export default function () {} 2 | @@ -34,6 +37,7 @@ Next.js can't recognize the exported \`config\` field in route. Proxy does not s 4 | The exported configuration object in a source file needs to have a very specific format from which some properties can be statically parsed at compiled-time.`) + } } else { expect(cliOutput).toContain( `Route segment config is not allowed in Proxy file at "./proxy.ts". Proxy always runs on Node.js runtime. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` From 745ae96b384c2f4cd77b3c24ce8d003412e30f3e Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Oct 2025 08:54:58 -0700 Subject: [PATCH 11/20] Always log from get-page-static as dev-turbo has log bug --- packages/next/src/build/analysis/get-page-static-info.ts | 3 +-- test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 54a07c1c62acd..781765c3e95dd 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -768,8 +768,7 @@ export async function getPagesPageStaticInfo({ let resolvedRuntime = config.runtime ?? config.config?.runtime - // Turbopack will handle error during compilation with better code frame. - if (!process.env.TURBOPACK && isProxyFile(page) && resolvedRuntime) { + if (isProxyFile(page) && resolvedRuntime) { const relativePath = relative(process.cwd(), pageFilePath) const resolvedPath = relativePath.startsWith('.') ? relativePath diff --git a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts index a45e34ae6fcc0..69c12bc2b8e79 100644 --- a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts +++ b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts @@ -24,11 +24,9 @@ describe('proxy-missing-export', () => { cliOutput = (await next.build()).cliOutput } - if (process.env.IS_TURBOPACK_TEST) { - if (isNextDev) { - // TODO: Investigate why in dev-turbo, the error is shown in the browser console, not CLI output. - } else { - expect(stripAnsi(cliOutput)).toContain(`proxy.ts:3:14 + // TODO: Investigate why in dev-turbo, the error is shown in the browser console, not CLI output. + if (process.env.IS_TURBOPACK_TEST && !isNextDev) { + expect(stripAnsi(cliOutput)).toContain(`proxy.ts:3:14 Next.js can't recognize the exported \`config\` field in route. Proxy does not support Edge runtime. 1 | export default function () {} 2 | @@ -37,7 +35,6 @@ Next.js can't recognize the exported \`config\` field in route. Proxy does not s 4 | The exported configuration object in a source file needs to have a very specific format from which some properties can be statically parsed at compiled-time.`) - } } else { expect(cliOutput).toContain( `Route segment config is not allowed in Proxy file at "./proxy.ts". Proxy always runs on Node.js runtime. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` From 73812f3609b6113f794afa0eb3f4c78ad5a942af Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Oct 2025 08:59:19 -0700 Subject: [PATCH 12/20] add comment why errorOnce --- packages/next/src/build/analysis/get-page-static-info.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 781765c3e95dd..f4a9481679611 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -415,6 +415,8 @@ function validateMiddlewareProxyExports({ `Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` if (isDev) { + // errorOnce as proxy/middleware runs per request including multiple + // internal _next/ routes and spams logs. Log.errorOnce(message) } else { throw new Error(message) @@ -776,6 +778,8 @@ export async function getPagesPageStaticInfo({ const message = `Route segment config is not allowed in Proxy file at "${resolvedPath}". Proxy always runs on Node.js runtime. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` if (isDev) { + // errorOnce as proxy/middleware runs per request including multiple + // internal _next/ routes and spams logs. Log.errorOnce(message) resolvedRuntime = SERVER_RUNTIME.nodejs } else { @@ -795,7 +799,7 @@ export async function getPagesPageStaticInfo({ ) { const message = `Page ${page} provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.` if (isDev) { - Log.errorOnce(message) + Log.error(message) } else { throw new Error(message) } From 8d1c3bb02c11536cd3d3062da5a6859f2907a610 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Oct 2025 09:39:04 -0700 Subject: [PATCH 13/20] test: Remove URLPattern usage --- test/production/proxy-typescript/app/proxy.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/production/proxy-typescript/app/proxy.ts b/test/production/proxy-typescript/app/proxy.ts index 85db0de0aa0d5..fcdb0047720ba 100644 --- a/test/production/proxy-typescript/app/proxy.ts +++ b/test/production/proxy-typescript/app/proxy.ts @@ -1,11 +1,6 @@ -import { NextProxy, NextResponse, URLPattern, ProxyConfig } from 'next/server' +import { NextProxy, NextResponse, ProxyConfig } from 'next/server' export const proxy: NextProxy = function (request) { - const pattern = new URLPattern({ - pathname: '/:path', - }) - console.log(pattern.test(request.nextUrl.pathname)) - if (request.nextUrl.pathname === '/static') { return new NextResponse(null, { headers: { From 1ec9b46c015d5e16794301b1209af5a7127061bf Mon Sep 17 00:00:00 2001 From: Jiwon Choi Date: Tue, 21 Oct 2025 09:54:36 -0700 Subject: [PATCH 14/20] fix text on describe and it --- test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts index 69c12bc2b8e79..ca90133c08433 100644 --- a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts +++ b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts @@ -1,7 +1,7 @@ import { nextTestSetup } from 'e2e-utils' import stripAnsi from 'strip-ansi' -describe('proxy-missing-export', () => { +describe('proxy-runtime', () => { const { next, isNextDev, skipped } = nextTestSetup({ files: __dirname, skipDeployment: true, @@ -12,7 +12,7 @@ describe('proxy-missing-export', () => { return } - it('should error when proxy file has invalid export named middleware', async () => { + it('should error when proxy file has runtime config export', async () => { let cliOutput: string if (isNextDev) { From f64edae7a829afd52c253bb6c6cc7818e7f7c370 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Oct 2025 12:03:13 -0700 Subject: [PATCH 15/20] add handler function check --- packages/next/errors.json | 3 ++- packages/next/src/build/templates/middleware.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/next/errors.json b/packages/next/errors.json index cbe2d5256c427..fe9759e8d65ee 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -901,5 +901,6 @@ "900": "Both %s file \"./%s\" and %s file \"./%s\" are detected. Please use \"./%s\" only. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy", "901": "Invalid \"cacheHandlers\" provided, expected an object e.g. { default: '/my-handler.js' }, received %s", "902": "Invalid handler fields configured for \"cacheHandlers\":\\n%s", - "903": "The file \"%s\" must export a function, either as a default export or as a named \"%s\" export.\\nThis function is what Next.js runs for every request handled by this %s.\\n\\nWhy this happens:\\n%s- The file exists but doesn't export a function.\\n- The export is not a function (e.g., an object or constant).\\n- There's a syntax error preventing the export from being recognized.\\n\\nTo fix it:\\n- Ensure this file has either a default or \"%s\" function export.\\n\\nLearn more: https://nextjs.org/docs/messages/middleware-to-proxy" + "903": "The file \"%s\" must export a function, either as a default export or as a named \"%s\" export.\\nThis function is what Next.js runs for every request handled by this %s.\\n\\nWhy this happens:\\n%s- The file exists but doesn't export a function.\\n- The export is not a function (e.g., an object or constant).\\n- There's a syntax error preventing the export from being recognized.\\n\\nTo fix it:\\n- Ensure this file has either a default or \"%s\" function export.\\n\\nLearn more: https://nextjs.org/docs/messages/middleware-to-proxy", + "904": "The file \"%s\" must export a function, either as a default export or as a named \"%s\" export." } diff --git a/packages/next/src/build/templates/middleware.ts b/packages/next/src/build/templates/middleware.ts index fa78a1f64ca11..599b432b083e8 100644 --- a/packages/next/src/build/templates/middleware.ts +++ b/packages/next/src/build/templates/middleware.ts @@ -15,6 +15,22 @@ const page: string = 'VAR_DEFINITION_PAGE' const isProxy = page === '/proxy' || page === '/src/proxy' const handler = (isProxy ? mod.proxy : mod.middleware) || mod.default +class ProxyMissingExportError extends Error { + constructor(message: string) { + super(message) + // Stack isn't useful here, remove it considering it spams logs during development. + this.stack = '' + } +} + +// TODO: This spams logs during development. Find a better way to handle this. +// Removing this will spam "fn is not a function" logs which is worse. +if (typeof handler !== 'function') { + throw new ProxyMissingExportError( + `The ${isProxy ? 'Proxy' : 'Middleware'} file "${page}" must export a function named \`${isProxy ? 'proxy' : 'middleware'}\` or a default function.` + ) +} + // Proxy will only sent out the FetchEvent to next server, // so load instrumentation module here and track the error inside proxy module. function errorHandledHandler(fn: AdapterOptions['handler']) { From 648e476c026698e90c86d2506f56cd9003930fa6 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:06:36 -0700 Subject: [PATCH 16/20] isdev --- Cargo.lock | 1 - packages/next/src/build/analysis/get-page-static-info.ts | 6 +++--- test/unit/parse-page-static-info.test.ts | 5 +++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efa500f3d8c07..cca56b629f964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4322,7 +4322,6 @@ dependencies = [ "next-custom-transforms", "next-taskless", "once_cell", - "pathdiff", "percent-encoding", "qstring", "react_remove_properties", diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index f4a9481679611..fe1fae37a4460 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -601,7 +601,7 @@ function warnAboutUnsupportedValue( type GetPageStaticInfoParams = { pageFilePath: string nextConfig: Partial - isDev?: boolean + isDev: boolean page: string pageType: PAGE_TYPES } @@ -629,7 +629,7 @@ export async function getAppPageStaticInfo({ ast, page, pageFilePath, - isDev: isDev ?? process.env.NODE_ENV === 'development', + isDev, }) const { @@ -730,7 +730,7 @@ export async function getPagesPageStaticInfo({ ast, page, pageFilePath, - isDev: isDev ?? process.env.NODE_ENV === 'development', + isDev, }) const { getServerSideProps, getStaticProps, exports } = checkExports( diff --git a/test/unit/parse-page-static-info.test.ts b/test/unit/parse-page-static-info.test.ts index e1282b97336de..df004408407f3 100644 --- a/test/unit/parse-page-static-info.test.ts +++ b/test/unit/parse-page-static-info.test.ts @@ -16,6 +16,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/nodejs-ssr.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, }) expect(runtime).toBe('nodejs') expect(getServerSideProps).toBe(true) @@ -29,6 +30,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/nodejs.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, }) expect(runtime).toBe('nodejs') expect(getServerSideProps).toBe(false) @@ -41,6 +43,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/edge.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, }) expect(runtime).toBe('experimental-edge') }) @@ -51,6 +54,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/static.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, }) expect(runtime).toBe(undefined) }) @@ -62,6 +66,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/ssr-variable-gssp.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, } ) expect(getStaticProps).toBe(false) From 9b25f18eebdd16a5a054ae55b625165c6cc940c0 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:52:35 -0700 Subject: [PATCH 17/20] Fix proxy renaming --- packages/next/src/build/entries.ts | 22 ++++---------- .../get-static-info-including-layouts.ts | 2 +- packages/next/src/server/next-server.ts | 29 ++++--------------- 3 files changed, 12 insertions(+), 41 deletions(-) diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index d9055754e3708..253fca01f2dfd 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -20,8 +20,6 @@ import { APP_DIR_ALIAS, WEBPACK_LAYERS, INSTRUMENTATION_HOOK_FILENAME, - PROXY_FILENAME, - MIDDLEWARE_FILENAME, } from '../lib/constants' import { isAPIRoute } from '../lib/is-api-route' import { isEdgeRuntime } from '../lib/is-edge-runtime' @@ -574,7 +572,7 @@ export interface CreateEntrypointsParams { buildId: string config: NextConfigComplete envFiles: LoadedEnvFiles - isDev?: boolean + isDev: boolean pages: MappedPages pagesDir?: string previewMode: __ApiPreviewProps @@ -643,6 +641,7 @@ export function getEdgeServerEntry(opts: { return { import: `next-middleware-loader?${stringify(loaderParams)}!`, layer: WEBPACK_LAYERS.middleware, + filename: 'middleware.js', } } @@ -770,7 +769,7 @@ export function runDependingOnPageType(params: { return } - if (isMiddlewareFile(params.page) && !isProxyFile(params.page)) { + if (isMiddlewareFile(params.page)) { if (params.pageRuntime === 'nodejs') { params.onServer() return @@ -953,13 +952,7 @@ export async function createEntrypoints( isDev: false, }) } else if (isMiddlewareFile(page)) { - server[ - serverBundlePath - // proxy.js still uses middleware.js for bundle path for now. - // TODO: Revisit when we remove middleware.js. - .replace(PROXY_FILENAME, MIDDLEWARE_FILENAME) - .replace('src/', '') - ] = getEdgeServerEntry({ + server[serverBundlePath.replace('src/', '')] = getEdgeServerEntry({ ...params, rootDir, absolutePagePath: absolutePagePath, @@ -1034,12 +1027,7 @@ export async function createEntrypoints( : undefined, }).import } - const edgeServerBundlePath = isMiddlewareFile(page) - ? serverBundlePath - .replace(PROXY_FILENAME, MIDDLEWARE_FILENAME) - .replace('src/', '') - : serverBundlePath - edgeServer[edgeServerBundlePath] = getEdgeServerEntry({ + edgeServer[serverBundlePath] = getEdgeServerEntry({ ...params, rootDir, absolutePagePath: absolutePagePath, diff --git a/packages/next/src/build/get-static-info-including-layouts.ts b/packages/next/src/build/get-static-info-including-layouts.ts index cf04c55374931..260bccb115ff4 100644 --- a/packages/next/src/build/get-static-info-including-layouts.ts +++ b/packages/next/src/build/get-static-info-including-layouts.ts @@ -29,7 +29,7 @@ export async function getStaticInfoIncludingLayouts({ pageFilePath: string appDir: string | undefined config: NextConfigComplete - isDev: boolean | undefined + isDev: boolean page: string }): Promise { // TODO: sync types for pages: PAGE_TYPES, ROUTER_TYPE, 'app' | 'pages', etc. diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index f5f16c3d266ad..8616e7326612b 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1578,30 +1578,13 @@ export default class NextNodeServer extends BaseServer< // of mapping proxy to middleware as the entry, just fallback // to proxy. // TODO: Remove this once we handle as the single entrypoint. - try { - return require( - join( - /* turbopackIgnore: true */ this.distDir, - 'server', - 'middleware.js' - ) + return require( + join( + /* turbopackIgnore: true */ this.distDir, + 'server', + 'middleware.js' ) - } catch (middlewareErr) { - if ( - isError(middlewareErr) && - (middlewareErr.code === 'ENOENT' || - middlewareErr.code === 'MODULE_NOT_FOUND') - ) { - return require( - join( - /* turbopackIgnore: true */ this.distDir, - 'server', - 'proxy.js' - ) - ) - } - throw middlewareErr - } + ) } } catch (err) { if ( From 470a4eec53ec037a6ec302c912b05782fd3ef6b3 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:35:53 -0700 Subject: [PATCH 18/20] fix dev --- packages/next/src/build/entries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 253fca01f2dfd..7f1f17f53784a 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -641,7 +641,7 @@ export function getEdgeServerEntry(opts: { return { import: `next-middleware-loader?${stringify(loaderParams)}!`, layer: WEBPACK_LAYERS.middleware, - filename: 'middleware.js', + filename: opts.isDev ? 'middleware.js' : undefined, } } From 1ba8d9064ecf3ca21ecf72ffe0489b0bfede2e17 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:39:40 -0700 Subject: [PATCH 19/20] Rename proxy chunk after the fact Co-Authored-By: JJ Kasper --- packages/next/src/build/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index b52d2c1959632..66130ae8946da 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -4152,6 +4152,17 @@ export default async function build( buildTracesSpinner = undefined } + if (proxyFilePath) { + await fs.rename( + path.join(distDir, SERVER_DIRECTORY, 'proxy.js'), + path.join(distDir, SERVER_DIRECTORY, 'middleware.js') + ) + await fs.rename( + path.join(distDir, SERVER_DIRECTORY, 'proxy.js.nft.json'), + path.join(distDir, SERVER_DIRECTORY, 'middleware.js.nft.json') + ) + } + if (isCompileMode) { Log.info( `Build ran with "compile" mode, to finalize the build run either "generate" or "generate-env" mode as well` From 9e8dbc9f62a0b07362274e9be3a9b8abf9e2c5eb Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:48:39 -0700 Subject: [PATCH 20/20] only rename for webpack --- packages/next/src/build/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 66130ae8946da..c9e6b1326473f 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -4152,7 +4152,7 @@ export default async function build( buildTracesSpinner = undefined } - if (proxyFilePath) { + if (proxyFilePath && bundler !== Bundler.Turbopack) { await fs.rename( path.join(distDir, SERVER_DIRECTORY, 'proxy.js'), path.join(distDir, SERVER_DIRECTORY, 'middleware.js')