vercel / vercel/vercel-plugin

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

Open Beginner friendly
#117 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
287
Forks
58
Avg merge
1d 1h
Merged PRs (30d)
17

Description

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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with skills/nextjs/references/file-conventions.md in both plugins and the corresponding upstream references, checking the matcher example and v16 table. Compare the documented export with Next 16's config-based documentation, then use unstable_doesMiddlewareMatch or the compiled proxy registration to verify the matcher is attached. Done means all four copies consistently document config and explain the file/function rename asymmetry.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js
Domain
documentation
Issue type
Documentation
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.