Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Only allow edge runtime in proxy
  • Loading branch information
mischnic committed Oct 21, 2025
commit d74b2cc534bcfda7d4bf6104c36167ab5b0b3095
20 changes: 16 additions & 4 deletions crates/next-api/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down
2 changes: 1 addition & 1 deletion crates/next-core/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ pub async fn get_middleware_module(
asset_context: Vc<Box<dyn AssetContext>>,
project_root: FileSystemPath,
userland_module: ResolvedVc<Box<dyn Module>>,
is_proxy: bool,
) -> Result<Vc<Box<dyn Module>>> {
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 {
Expand Down
46 changes: 31 additions & 15 deletions crates/next-core/src/segment_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 =
Expand Down
12 changes: 12 additions & 0 deletions packages/next/src/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
28 changes: 1 addition & 27 deletions packages/next/src/build/templates/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']) {
Expand Down
8 changes: 8 additions & 0 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/app-dir/proxy-runtime/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/proxy-runtime/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
6 changes: 6 additions & 0 deletions test/e2e/app-dir/proxy-runtime/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
43 changes: 43 additions & 0 deletions test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
3 changes: 3 additions & 0 deletions test/e2e/app-dir/proxy-runtime/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function () {}

export const config = { runtime: 'edge' }