Skip to content

nextjs skill: matcher export is config, not proxyConfig — Next 16 silently ignores the matcher #117

Description

@tanyaabenaim

Summary

The nextjs skill teaches that Next.js 16 renamed the middleware matcher export from config to proxyConfig. It did not. Next 16 renamed only the file (middleware.tsproxy.ts) and the handler function (middleware()proxy()). The matcher export is still config.

Because Next reads that binding by name during static analysis at build time, an export named proxyConfig is never read: the matcher is silently ignored and the proxy runs on every request, including _next/static, _next/image, favicon.ico, and image assets that the matcher was written to exclude. There is no build error and no warning.

This is a "fails safe, but silently wrong" bug: the proxy over-runs, never under-runs, so nothing becomes under-protected — but any per-request work in the proxy (auth checks, session refresh, remote calls) now executes on every static asset request, and the author believes it does not.

An agent following this skill will write a proxy whose matcher does nothing.

Affected files

Both distributed plugins ship the same content:

  • vercel 0.44.0 (claude-plugins-official) — skills/nextjs/references/file-conventions.md
  • vercel-plugin 0.32.3 (vercel-labs) — skills/nextjs/references/file-conventions.md

(and the corresponding skills/nextjs/upstream/references/file-conventions.md in each)

Offending lines, identical in both:

126: export const proxyConfig = {
127:   matcher: ['/dashboard/:path*', '/api/:path*'],
128: };
134: | v16+ | `proxy.ts` | `proxy()` | `proxyConfig` |

Evidence

1. Official Next.js documentation. The canonical proxy.ts example in the Next 16 docs (docs/01-app/03-api-reference/03-file-conventions/proxy.mdx, also shipped inside the npm package at next/dist/docs/.../proxy.md) uses:

export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

The docs' own unit-testing example likewise passes a variable named config.

2. The compiler reads the literal string config. In next@16.2.9, dist/build/analysis/get-page-static-info.js extracts the matcher via:

extractExportedConstValue(ast, 'config')

and feeds the result to getMiddlewareMatchers(...). grep -rn "proxyConfig" node_modules/next/dist/ returns zero matches — the identifier does not exist anywhere in Next's source.

3. Differential build. Same proxy file, same matcher array, only the export identifier changed; built with next build (Turbopack) on next@16.2.9.

Reproducer — proxy.ts at project root:

import { NextResponse, type NextRequest } from 'next/server'

export function proxy(_request: NextRequest) {
  return NextResponse.next()
}

// Variant A (as the skill teaches):
export const proxyConfig = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

// Variant B (correct):
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Inspecting the compiled proxy registration in .next/server/chunks/:

  • Variant A (proxyConfig)e.s(["proxy",0 — no matcher object is attached to the registration, and no chunk associates a matcher with "proxy" at all. The matcher array survives in the bundle only as a dead constant.
  • Variant B (config)matcher:["/((?!_next/static|_next/image|favicon.ico).*)"]},"proxy",0 — the matcher is attached.

4. Runtime confirmation via Next's own matcher oracle. Using unstable_doesMiddlewareMatch from next/experimental/testing/server with the exported config:

URL matches
/_next/static/chunks/main.js false
/favicon.ico false
/dashboard true

With the export named proxyConfig, there is no config binding to hand the oracle at all.

A note for anyone verifying this

.next/server/middleware-manifest.json is not a reliable oracle under a Turbopack build. It stays an empty stub ({"middleware": {}, "sortedMiddleware": [], "functions": {}}) even when the proxy is compiled and its matcher is correctly applied. Checking that file will make a correct fix look broken. Use the compiled chunk registration or unstable_doesMiddlewareMatch instead.

Suggested fix

In skills/nextjs/references/file-conventions.md (both copies, in both plugins):

  • Line 126: export const proxyConfig = {export const config = {

  • Line 134: | v16+ | \proxy.ts` | `proxy()` | `config` (unchanged) |`

  • Add a note under the version table, since the asymmetry is the whole trap:

    Only the file and the function were renamed in v16. The matcher export is still config — Next reads that binding by name at build time, so an export named proxyConfig is never read and the matcher is silently ignored.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions