Skip to content

fix(vite): route asset-tagged requests to opaque catch-alls in dev - #4467

Merged
pi0 merged 8 commits into
mainfrom
vite/vite-dev
Jul 22, 2026
Merged

fix(vite): route asset-tagged requests to opaque catch-alls in dev#4467
pi0 merged 8 commits into
mainfrom
vite/vite-dev

Conversation

@pi0x

@pi0x pi0x commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🔗 Linked issue

resolves #4252, resolves TanStack/router#7403 (related: #4234, #4241, #4266, #4270, #4272, #4274)

📚 Description

Routes owned by an opaque catch-all — a custom server.ts server entry (an H3 app with its own routes) or the SSR renderer (framework routers like TanStack Start) — are invisible to nitro.routing.routes. The dev middleware classifier therefore treated asset-tagged requests to them (sec-fetch-dest: image, asset extensions, …) as Vite assets and marked them _nitroHandled, so the user's handler could never run: <img src="/image.png"> served by a server.ts route, or a TanStack Start API route serving a thumbnail, died in a connect finalhandler 404.

The two cases (/image.png is a real opaque route vs. a genuinely missing asset a naive SSR /** would render as a 200 page) cannot be told apart before dispatching — but they can from the response:

  • The pre-pass now splits non-explicit matches into transparent catch-alls (user route files, e.g. routes/[...path].ts — Nitro sees everything they can handle, so the existing _nitroHandled divert stays and they still never swallow Vite asset misses) and opaque ones (renderer / serverEntry /**, identified by their registered handler paths — or no match at all).
  • Opaque/no-match asset requests are stamped _nitroAssetCheck instead: Vite keeps first shot, and on a Vite miss the post catch-all dispatches to nitro and inspects the response. A 2xx text/html response means the catch-all rendered a page for a missing asset (Accessing via a non-localhost URL breaks vite dev server #4234) → discarded via next() (same finalhandler 404 as before). Anything else — real asset types, JSON (deliberate API responses, File extension/Image src does not end to server route but 404 TanStack/router#7403; sourcemaps), text/plain (bridge default for bare string returns), no content-type, non-2xx framework 404 pages, redirects — passes through verbatim.
  • Only GET/HEAD requests can be classified as asset loads at all (a POST /upload.png is never a browser asset load).

Zero config, host-side only: no runtime/bundle changes and no production behavior change. All #4234/#4241/#4266/#4270/#4272 regression contracts stay covered by the existing tests. Verified against a real @tanstack/react-start app: API routes under sec-fetch-dest: image (extensionless, .png-param, and ?filename=x.png shapes from #7403 / #4274) now return their JSON 200s, while genuinely missing assets and unmatched routes still 404.

New regression tests (fail before, pass after, both builders):

  • test/vite/server-entry.test.ts + fixture — custom server.ts H3 app: asset-extensioned routes reachable under image/absent/script fetch dests (including a no-content-type string return and a JSON sourcemap), POST to an asset-extensioned route, missing assets still 404, navigations work.
  • test/vite/app.test.ts — the SSR catch-all can deliberately serve /dynamic-asset.png (image/png) and JSON API routes under sec-fetch-dest: image (#7403 shapes), while the missing-asset swallow contracts still hold against an HTML-rendering fixture.

Known limitation (documented): opaque semantics require registration via renderer/serverEntry — a /** catch-all added through routes:/handlers: config or a module is still classified transparent and pre-empted for asset-tagged requests.

Also includes .agents/vite-dev.md documenting the routing design, its issue lineage, and the invariants to preserve.

🤖 Generated with Claude Code

Routes owned by a custom server entry or the SSR renderer are invisible to
`nitro.routing.routes`, so the dev middleware pre-empted asset-tagged requests
to them with `_nitroHandled` and they could never run (#4252). Dispatch such
requests to nitro after Vite declines and decide from the response instead: a
2xx page/data content-type means the catch-all swallowed a missing asset
(#4234) and falls through to the 404; anything else passes through verbatim.
User-file root catch-alls keep the existing divert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pi0x
pi0x requested a review from pi0 as a code owner July 22, 2026 18:30
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nitro.build Ready Ready Preview, Comment Jul 22, 2026 7:39pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Vite dev routing

Layer / File(s) Summary
Opaque catch-all dispatch and response inspection
src/build/vite/dev.ts
Adds opaque-handler classification, _nitroAssetCheck, GET/HEAD asset gating, page-content inspection, and middleware fallthrough for successful HTML responses from asset-tagged requests.
Custom server-entry fixture and routing coverage
test/vite/server-entry-fixture/*, test/vite/server-entry.test.ts, test/vite/app-fixture/app/entry-server.ts, test/vite/app.test.ts
Adds server-entry routes and tests navigation, assets, scripts, sourcemaps, uploads, missing assets, and dynamic SSR assets.
Routing rules and implementation coverage
.agents/vite-dev.md
Documents middleware ordering, route classification, asset heuristics, response inspection, dispatch flow, test coverage, and runtime constraints.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

  • nitrojs/nitro#4108 — Modifies the same Vite dev request-routing middleware and Nitro fallback classification.
  • nitrojs/nitro#4238 — Adjusts asset/document classification in the same middleware.
  • nitrojs/nitro#4453 — Addresses SSR catch-all handling for asset-like requests in the same middleware.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits and clearly summarizes the Vite dev routing change.
Description check ✅ Passed The description is on-topic and matches the asset-tagged opaque catch-all routing updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vite/vite-dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/nitro@4467

commit: 323e0f1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/vite/server-entry.test.ts`:
- Around line 74-85: Update the “missing assets still 404” test to assert that
each request returns status 404, replacing the weaker non-200 assertion while
preserving the existing headers, URL, and manual redirect behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d16b69ff-e79c-4822-8166-22baf1db7219

📥 Commits

Reviewing files that changed from the base of the PR and between 0192bba and ac26576.

📒 Files selected for processing (7)
  • .agents/vite-dev.md
  • src/build/vite/dev.ts
  • test/vite/app-fixture/app/entry-server.ts
  • test/vite/app.test.ts
  • test/vite/server-entry-fixture/server.ts
  • test/vite/server-entry-fixture/vite.config.ts
  • test/vite/server-entry.test.ts

Comment on lines +74 to +85
// #4234 contract: a genuinely missing asset must still not be answered with 200.
test("missing assets still 404", async () => {
for (const headers of [{ "sec-fetch-dest": "style" }, { accept: "*/*" }] as Record<
string,
string
>[]) {
const res = await fetch(`${serverURL}/missing-asset.css`, {
headers,
redirect: "manual",
});
expect(res.status, JSON.stringify(headers)).not.toBe(200);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the promised 404 status.

Lines 74-85 only reject 200, so incorrect non-200 responses would pass. The test and routing contract both specify a 404 fallback.

Proposed fix
-      expect(res.status, JSON.stringify(headers)).not.toBe(200);
+      expect(res.status, JSON.stringify(headers)).toBe(404);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// #4234 contract: a genuinely missing asset must still not be answered with 200.
test("missing assets still 404", async () => {
for (const headers of [{ "sec-fetch-dest": "style" }, { accept: "*/*" }] as Record<
string,
string
>[]) {
const res = await fetch(`${serverURL}/missing-asset.css`, {
headers,
redirect: "manual",
});
expect(res.status, JSON.stringify(headers)).not.toBe(200);
}
// `#4234` contract: a genuinely missing asset must still not be answered with 200.
test("missing assets still 404", async () => {
for (const headers of [{ "sec-fetch-dest": "style" }, { accept: "*/*" }] as Record<
string,
string
>[]) {
const res = await fetch(`${serverURL}/missing-asset.css`, {
headers,
redirect: "manual",
});
expect(res.status, JSON.stringify(headers)).toBe(404);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/vite/server-entry.test.ts` around lines 74 - 85, Update the “missing
assets still 404” test to assert that each request returns status 404, replacing
the weaker non-200 assertion while preserving the existing headers, URL, and
manual redirect behavior.

Hoist opaque catch-all detection into an isOpaqueHandler helper (dropping
the per-request Set), share a NitroDevRequest type between both middlewares
(removing the inline cast), and drop the intermediate array in the
mounted-path skip. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cemaps through

Non-GET/HEAD requests are never browser asset loads, so a POST to an
asset-extensioned URL now bypasses the asset classification. For `.map` URLs
only text/html counts as a catch-all swallow, since sourcemaps are
legitimately application/json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opaque frameworks answer API routes tagged as asset loads with JSON on
purpose (TanStack/router#7403), and sourcemaps are legitimately JSON, so
narrow the response inspection deny to text/html. A naive SSR entry
swallowing missing assets renders an HTML page, which is still coerced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/vite/app-fixture/app/entry-server.ts`:
- Around line 17-22: Align the fallback condition in the entry-server handler
with its documented SSR behavior: either broaden the predicate so extensionless
unmatched pathnames also return the HTML Response, or revise the comment to
state that only extensioned paths are handled. Preserve the existing JSON
fallback for paths excluded by the chosen behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fea9441c-ee24-4ec9-83af-68cca69c7d3e

📥 Commits

Reviewing files that changed from the base of the PR and between 0c2a043 and b1309c0.

📒 Files selected for processing (4)
  • .agents/vite-dev.md
  • src/build/vite/dev.ts
  • test/vite/app-fixture/app/entry-server.ts
  • test/vite/app.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • .agents/vite-dev.md
  • src/build/vite/dev.ts

Comment thread test/vite/app-fixture/app/entry-server.ts
pi0 and others added 3 commits July 22, 2026 18:57
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pi0
pi0 merged commit 1dfe4e8 into main Jul 22, 2026
14 checks passed
@pi0
pi0 deleted the vite/vite-dev branch July 22, 2026 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File extension/Image src does not end to server route but 404 Vite dev middleware swallows asset-extensioned URLs

2 participants