Vite dev: config-registered /** catch-all handlers (routes:/handlers:) are classified transparent and never dispatched for asset-tagged requests
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 11.2k
- Forks
- 899
- Avg merge
- 2d 24m
- Merged PRs (30d)
- 40
Description
Environment
- nitro: 3.0.260610-beta
- Node.js: 24.x
Reproduction
nitro.config.ts:
export default defineNitroConfig({
routes: {
"/**": { handler: "~/server/catchall.ts" },
},
});
server/catchall.ts serving a binary response for any path, e.g. a generated PNG:
export default defineHandler((event) => {
return send(event, generatePng(event.path), "image/png");
});
Request it in dev with an <img src="/whatever.png"> tag (browser sends Sec-Fetch-Dest: image), or with curl -H "Sec-Fetch-Dest: image" http://localhost:3000/whatever.png.
What happens
- The same request without the
Sec-Fetch-Destheader, or in production, works fine.
Why
I traced this to src/build/vite/dev.ts's nitroDevMiddlewarePre, which splits ambiguous catch-all matches into "opaque" (Nitro-only, a Vite miss must still be dispatched to Nitro) and "transparent" (Nitro sees everything, so a Vite miss must not fall back into it). The check is:
const isOpaqueHandler = (h?: { handler?: string }) =>
!!h?.handler &&
(h.handler === nitro.options.renderer?.handler ||
h.handler === (nitro.options.serverEntry && nitro.options.serverEntry.handler));
This only recognizes renderer/serverEntry handlers. A /** catch-all registered through routes: or handlers: in nitro.config merges into nitro.routing.routes with the same route: "/**" shape as a file-based catch-all (src/routing.ts), so it isn't an "explicit route" either. It falls into the ambiguous bucket, isOpaqueHandler returns false for it, and it gets classified transparent:
if (matchedHandlers.every(isOpaqueHandler)) {
req._nitroAssetCheck = true;
} else {
req._nitroHandled = true;
}
_nitroHandled = true makes nitroDevMiddleware return next() immediately without ever calling ctx.devApp!.fetch(req), confirmed reading the earlier block in the same file where _nitroHandled short-circuits before the fetch call. So the catch-all handler never runs for asset-tagged requests, and the request 404s through Vite/connect's finalhandler.
The renderer/serverEntry cases were fixed for exactly this class of bug in #4467, but a config-registered /** catch-all hits the same wall. It's a real registration path (routes:/handlers: with a wildcard key/route, no framework SSR involved) and I'd expect it to behave like a routes/[...path].ts file, except a file-based root catch-all is deliberately kept "transparent" on purpose (Nitro's nitro.routing.routes sees it completely, so Vite is authoritative for asset misses), whereas this config path can't be told apart from that case even though the underlying handler is opaque to Vite the same way serverEntry is.
Suggested fix
isOpaqueHandler needs a way to recognize config-registered catch-alls as opaque too, or the config path needs to be marked at registration time (similar to how serverEntry/renderer are tagged) rather than inferred purely from h.handler identity.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/build/vite/dev.ts at nitroDevMiddlewarePre and its isOpaqueHandler check, then compare the route shapes assembled in src/routing.ts and review the related fix in #4467. Reproduce with a config-registered /** handler and Sec-Fetch-Dest: image; done means the handler is dispatched in Vite dev and the regression is covered by an appropriate test.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript, vite
- Domain
- backend, build-system, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100