Repository: vercel/next.js
Pull request: fix: invalid middleware configs should fail the build

In #68638 we intended to fail the build if an invalid matcher configuration was provided by importing a shared constant from the static page analysis fn. This worked fine for segment validations for pages/route handlers. However, the build would incorrectly succeed in the case of an invalid middleware config, because we were checking too early (the middleware is evaluated later). This is bad because it would actually ignore the invalid value, and instead fallback to a greedy catch-all match, which would invoke middleware for any path.

This updates the return shape of the static page info to signal whether an invalid configuration was detected rather than relying on a shared const, and adds errors to the appropriate places so that the build will correctly fail. This PR includes a test with invalid middleware to validate the behavior. 

Changed files:
filename: packages/next/src/build/analysis/get-page-static-info.ts
patch: @@ -79,6 +79,7 @@ export interface AppPageStaticInfo {
   runtime: AppSegmentConfig['runtime'] | undefined
   preferredRegion: AppSegmentConfig['preferredRegion'] | undefined
   maxDuration: number | undefined
+  hadUnsupportedValue: boolean
 }
 
 export interface PagesPageStaticInfo {
@@ -98,6 +99,7 @@ export interface PagesPageStaticInfo {
   runtime: PagesSegmentConfig['runtime'] | undefined
   preferredRegion: PagesSegmentConfigConfig['regions'] | undefined
   maxDuration: number | undefined
+  hadUnsupportedValue: boolean
 }
 
 export type PageStaticInfo = AppPageStaticInfo | PagesPageStaticInfo
@@ -426,7 +428,7 @@ function warnAboutExperimentalEdge(apiRoute: string | null) {
   apiRouteWarnings.set(apiRoute, 1)
 }
 
-export let hadUnsupportedValue = false
+let hadUnsupportedValue = false
 const warnedUnsupportedValueMap = new LRUCache<boolean>(250, () => 1)
 
 function warnAboutUnsupportedValue(
@@ -487,6 +489,7 @@ export async function getAppPageStaticInfo({
       runtime: undefined,
       preferredRegion: undefined,
       maxDuration: undefined,
+      hadUnsupportedValue: false,
     }
   }
 
@@ -552,6 +555,7 @@ export async function getAppPageStaticInfo({
     runtime: config.runtime,
     preferredRegion: config.preferredRegion,
     maxDuration: config.maxDuration,
+    hadUnsupportedValue,
   }
 }
 
@@ -569,6 +573,7 @@ export async function getPagesPageStaticInfo({
       runtime: undefined,
       preferredRegion: undefined,
       maxDuration: undefined,
+      hadUnsupportedValue: false,
     }
   }
 
@@ -634,6 +639,7 @@ export async function getPagesPageStaticInfo({
     runtime: resolvedRuntime,
     preferredRegion: config.config?.regions,
     maxDuration: config.maxDuration ?? config.config?.maxDuration,
+    hadUnsupportedValue,
   }
 }
 

filename: packages/next/src/build/index.ts
patch: @@ -110,7 +110,6 @@ import {
 } from '../telemetry/events'
 import type { EventBuildFeatureUsage } from '../telemetry/events'
 import { Telemetry } from '../telemetry/storage'
-import { hadUnsupportedValue } from './analysis/get-page-static-info'
 import {
   createPagesMapping,
   getStaticInfoIncludingLayouts,
@@ -1869,6 +1868,10 @@ export default async function build(
                     })
                   : undefined
 
+                if (staticInfo?.hadUnsupportedValue) {
+                  errorFromUnsupportedSegmentConfig()
+                }
+
                 // If there's any thing that would contribute to the functions
                 // configuration, we need to add it to the manifest.
                 if (
@@ -2183,13 +2186,6 @@ export default async function build(
             })
         )
 
-        if (hadUnsupportedValue) {
-          Log.error(
-            `Invalid config value exports detected, these can cause unexpected behavior from the configs not being applied. Please fix them to continue`
-          )
-          process.exit(1)
-        }
-
         const errorPageResult = await errorPageStaticResult
         const nonStaticErrorPage =
           (await errorPageHasCustomGetInitialProps) ||
@@ -2371,6 +2367,10 @@ export default async function build(
           page: 'middleware',
         })
 
+        if (staticInfo.hadUnsupportedValue) {
+          errorFromUnsupportedSegmentConfig()
+        }
+
         if (staticInfo.runtime === 'nodejs') {
           hasNodeMiddleware = true
           functionsConfigManifest.functions['/_middleware'] = {
@@ -3786,6 +3786,13 @@ export default async function build(
   }
 }
 
+function errorFromUnsupportedSegmentConfig(): never {
+  Log.error(
+    `Invalid segment configuration export detected. This can cause unexpected behavior from the configs not being applied. You should see the relevant failures in the logs above. Please fix them to continue.`
+  )
+  process.exit(1)
+}
+
 function warnAboutTurbopackBuilds(config?: NextConfigComplete) {
   let warningStr =
     `Support for Turbopack builds is experimental. ` +

filename: test/production/exported-runtimes-value-validation/index.test.ts
patch: @@ -16,7 +16,39 @@ describe('Exported runtimes value validation', () => {
     })
   })
 
-  test('warns on unrecognized runtimes value', async () => {
+  test('fails the build on invalid middleware matcher', async () => {
+    const result = await nextBuild(
+      path.resolve(__dirname, './invalid-middleware'),
+      undefined,
+      { stdout: true, stderr: true }
+    )
+
+    // The build should fail to prevent unexpected behavior
+    expect(result.code).toBe(1)
+
+    // TODO: Turbopack matches the error message but omits the routing & error information
+    if (process.env.IS_TURBOPACK_TEST) {
+      expect(result.stderr).toEqual(
+        expect.stringContaining(
+          "Next.js can't recognize the exported `config` field in route"
+        )
+      )
+    } else {
+      expect(result.stderr).toEqual(
+        expect.stringContaining(
+          'Next.js can\'t recognize the exported `config` field in route "/middleware"'
+        )
+      )
+
+      expect(result.stderr).toEqual(
+        expect.stringContaining(
+          'Unknown identifier "dynamicPath" at "config.matcher[1]"'
+        )
+      )
+    }
+  })
+
+  test('fails the build on unrecognized runtimes value', async () => {
     const result = await nextBuild(
       path.resolve(__dirname, './unsupported-syntax/app'),
       undefined,

filename: test/production/exported-runtimes-value-validation/invalid-middleware/middleware.js
patch: @@ -0,0 +1,5 @@
+const dynamicPath = `/:foobar`
+
+export const config = {
+  matcher: ['/foo', dynamicPath],
+}

filename: test/production/exported-runtimes-value-validation/invalid-middleware/pages/index.js
patch: @@ -0,0 +1,3 @@
+export default function Page() {
+  return <p>hello world</p>
+}