Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 40 additions & 15 deletions crates/next-core/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,23 +139,48 @@ impl Issue for MiddlewareMissingExportIssue {
}

#[turbo_tasks::function]
fn title(&self) -> Vc<StyledString> {
let file_name = self.file_path.file_name();

StyledString::Line(vec![
StyledString::Text(rcstr!("The ")),
StyledString::Code(self.file_type.clone()),
StyledString::Text(rcstr!(" file \"")),
StyledString::Code(format!("./{}", file_name).into()),
StyledString::Text(rcstr!("\" must export a function named ")),
StyledString::Code(format!("`{}`", self.function_name).into()),
StyledString::Text(rcstr!(" or a default function.")),
])
.cell()
async fn title(&self) -> Result<Vc<StyledString>> {
let title_text = format!(
"{} is missing expected function export name",
self.file_type
);

Ok(StyledString::Text(title_text.into()).cell())
}

#[turbo_tasks::function]
fn description(&self) -> Vc<OptionStyledString> {
Vc::cell(None)
async fn description(&self) -> Result<Vc<OptionStyledString>> {
let type_description = if self.file_type == "Proxy" {
"proxy (previously called middleware)"
} else {
"middleware"
};

let migration_bullet = if self.file_type == "Proxy" {
"- You are migrating from `middleware` to `proxy`, but haven't updated the exported \
function.\n"
} else {
""
};

// Rest of the message goes in description to avoid formatIssue indentation
let description_text = format!(
"This function is what Next.js runs for every request handled by this {}.\n\n\
Why this happens:\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 \"{}\" function export.\n\n\
Learn more: https://nextjs.org/docs/messages/middleware-to-proxy",
type_description,
migration_bullet,
self.function_name
);

Ok(Vc::cell(Some(
StyledString::Text(description_text.into()).resolved_cell(),
)))
}
}
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -900,5 +900,6 @@
"899": "Both \"%s\" and \"%s\" files are detected. Please use \"%s\" instead. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy",
"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"
"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"
}
19 changes: 16 additions & 3 deletions packages/next/src/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { NextConfig } from '../../server/config-shared'
import type { RouteHas } from '../../lib/load-custom-routes'

import { promises as fs } from 'fs'
import { basename } from 'path'
import { relative } from 'path'
import { LRUCache } from '../../server/lib/lru-cache'
import {
extractExportedConstValue,
Expand Down Expand Up @@ -329,7 +329,7 @@ function validateMiddlewareProxyExports({
return
}

const fileName = isProxy ? 'proxy' : 'middleware'
const fileName = isProxy ? PROXY_FILENAME : MIDDLEWARE_FILENAME

// Parse AST to get export info (since checkExports doesn't return middleware/proxy info)
let hasDefaultExport = false
Expand Down Expand Up @@ -396,9 +396,22 @@ function validateMiddlewareProxyExports({
(isMiddleware && hasMiddlewareExport) ||
(isProxy && hasProxyExport)

const relativeFilePath = `./${relative(process.cwd(), pageFilePath)}`

if (!hasValidExport) {
throw new Error(
`The ${fileName === 'proxy' ? 'Proxy' : 'Middleware'} file "./${basename(pageFilePath)}" must export a function named \`${fileName}\` or a default function.`
`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`
)
}
}
Expand Down
19 changes: 18 additions & 1 deletion packages/next/src/build/templates/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,25 @@ 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 ${isProxy ? 'Proxy' : 'Middleware'} file "${relativeFilePath.startsWith('.') ? relativeFilePath : `./${relativeFilePath}`}" must export a function named \`${isProxy ? 'proxy' : 'middleware'}\` or a default function.`
`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`
)
}

Expand Down
49 changes: 41 additions & 8 deletions test/e2e/app-dir/proxy-missing-export/proxy-missing-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@ import { nextTestSetup } from 'e2e-utils'
import { join } from 'node:path'
import { writeFile } from 'node:fs/promises'

const errorMessage =
'The Proxy file "./proxy.ts" must export a function named `proxy` or a default function.'
const errorMessage = `This function is what Next.js runs for every request handled by this proxy (previously called middleware).

Why this happens:
- You are migrating from \`middleware\` to \`proxy\`, but haven't updated the exported function.
- The file exists but doesn't export a function.
- The export is not a function (e.g., an object or constant).
- There's a syntax error preventing the export from being recognized.

To fix it:
- Ensure this file has either a default or "proxy" function export.

Learn more: https://nextjs.org/docs/messages/middleware-to-proxy`

describe('proxy-missing-export', () => {
const { next, isNextDev, skipped } = nextTestSetup({
Expand All @@ -22,14 +32,26 @@ describe('proxy-missing-export', () => {
'export function middleware() {}'
)

let cliOutput: string

if (isNextDev) {
await next.start().catch(() => {})
// Use .catch() because Turbopack errors during compile and exits before runtime.
await next.browser('/').catch(() => {})
expect(next.cliOutput).toContain(errorMessage)
cliOutput = next.cliOutput
} else {
cliOutput = (await next.build()).cliOutput
}

// 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(cliOutput).toContain(`./proxy.ts
Proxy is missing expected function export name
${errorMessage}`)
} else {
const { cliOutput } = await next.build()
expect(cliOutput).toContain(errorMessage)
expect(cliOutput)
.toContain(`The file "./proxy.ts" must export a function, either as a default export or as a named "proxy" export.
${errorMessage}`)
}

await next.stop()
Expand Down Expand Up @@ -94,16 +116,27 @@ describe('proxy-missing-export', () => {
'const proxy = () => {}; export { proxy as handler };'
)

let cliOutput: string

if (isNextDev) {
await next.start().catch(() => {})
// Use .catch() because Turbopack errors during compile and exits before runtime.
await next.browser('/').catch(() => {})
expect(next.cliOutput).toContain(errorMessage)
cliOutput = next.cliOutput
} else {
const { cliOutput } = await next.build()
expect(cliOutput).toContain(errorMessage)
cliOutput = (await next.build()).cliOutput
}

// 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(cliOutput).toContain(`./proxy.ts
Proxy is missing expected function export name
${errorMessage}`)
} else {
expect(cliOutput)
.toContain(`The file "./proxy.ts" must export a function, either as a default export or as a named "proxy" export.
${errorMessage}`)
}
await next.stop()
})
})
Loading