Skip to content

fix(dev): route specific Nitro paths even when URL has asset extension#4266

Closed
harshagarwalnyu wants to merge 1 commit into
nitrojs:mainfrom
harshagarwalnyu:fix/dev-middleware-asset-ext-swallows-routes
Closed

fix(dev): route specific Nitro paths even when URL has asset extension#4266
harshagarwalnyu wants to merge 1 commit into
nitrojs:mainfrom
harshagarwalnyu:fix/dev-middleware-asset-ext-swallows-routes

Conversation

@harshagarwalnyu
Copy link
Copy Markdown

Asset-extensioned URLs (e.g. /api/photos/12345.jpg) were routed to Vite's static middleware
instead of the matching Nitro handler. The ASSET_EXT_RE guard in nitroDevMiddlewarePre
applied unconditionally to any matched route, including specific prefixed catch-alls like /api/photos/**.

The guard was introduced (#4234) to prevent a root-level renderer or user catch-all (/**) from
swallowing Vite's own <script>/<link> serves on plain-HTTP non-loopback origins where
Sec-Fetch-Dest is absent.

Fix: narrow the guard so the asset-extension filter only applies when the matched Nitro route
pattern is exactly /**. Specific routes (/api/**, /api/photos/**) are never registered on
paths Vite owns and should always win — restoring the behaviour from nitro@3.0.1-alpha.2.

Test plan

Fixes #4252

Copilot AI review requested due to automatic review settings May 14, 2026 15:16
@harshagarwalnyu harshagarwalnyu requested a review from pi0 as a code owner May 14, 2026 15:16
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

@harshagarwalnyu is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d1223d3-3bba-4080-a0fd-7bb8fd2ebf0b

📥 Commits

Reviewing files that changed from the base of the PR and between adf0198 and bbea608.

📒 Files selected for processing (3)
  • src/build/vite/dev.ts
  • test/vite/baseurl-dotted-param-fixture/routes/[...path].ts
  • test/vite/baseurl-dotted-param.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/vite/baseurl-dotted-param-fixture/routes/[...path].ts
  • src/build/vite/dev.ts
  • test/vite/baseurl-dotted-param.test.ts

📝 Walkthrough

Walkthrough

The Vite dev middleware pre-routing computes the request pathname once, matches Nitro routes, sets asset-detection signals and Vary headers, and only suppresses Nitro for root wildcard matches (/**). Explicit/prefixed Nitro routes may handle asset-like requests. Tests and a root-wildcard fixture were added to assert prefixed-route precedence and root-wildcard non-interference.

Changes

Vite dev middleware route matching refinement

Layer / File(s) Summary
Middleware route matching and Nitro-win decision
src/build/vite/dev.ts
Derive reqPathname via URL(...).pathname, perform a single nitro.routing.routes.match(...) into nitroRouteMatch, compute isAssetByDest, isAssetByExt, acceptsHTML, treatAsAsset, set Vary: sec-fetch-dest, accept, and update the Nitro-win logic so asset-extension suppression applies only when the matched Nitro route is a root wildcard (/**).
Tests and fixture verifying prefixed vs root-wildcard behavior
test/vite/baseurl-dotted-param-fixture/routes/[...path].ts, test/vite/baseurl-dotted-param.test.ts
Add a root-wildcard route handler returning root-wildcard: + param and Vitest assertions that prefixed splat Nitro routes (e.g. /api/proxy/**) win over Vite asset handling when sec-fetch-dest is absent, and that root /** wildcards do not intercept Vite assets like /subdir/entry-client.ts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • nitrojs/nitro#4238: Both PRs update src/build/vite/dev.ts’s nitroDevMiddlewarePre to prevent misrouting Vite asset requests when Sec-Fetch-Dest is missing and extend test/vite/baseurl-dotted-param.test.ts.
  • nitrojs/nitro#4108: Both PRs modify configureViteDevServer’s nitroDevMiddlewarePre routing logic to adjust how nitro.routing.routes.match(...) is computed/used for baseURL/prefix paths and how that match affects Vite vs Nitro handling.
  • nitrojs/nitro#3817: Also changes src/build/vite/dev.ts within configureViteDevServer to adjust middleware decision/control-flow for when Nitro should handle requests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits format with 'fix' prefix and clearly describes the specific change: routing Nitro paths when URLs have asset extensions.
Description check ✅ Passed The description is directly related to the changeset, explaining the root cause of the regression, the fix applied, and the test plan for validation.
Linked Issues check ✅ Passed The pull request addresses all coding objectives from issue #4252: ensures matching Nitro routes override the asset-extension guard, applies the guard only for root-wildcard routes, avoids poisoning the fallback when Nitro routes match, and preserves the static-asset optimization.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the regression described in #4252 and its regression test #4234. Changes include dev middleware logic, a test fixture, and expanded test coverage—all directly addressing the linked issue objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes Nitro’s Vite dev-server routing precedence so that explicit Nitro routes (e.g. /api/**, /api/photos/**) still reach Nitro even when the request URL ends with an “asset-like” extension (e.g. .jpg), while preserving the original safeguard that prevents a root catch-all (/**) from swallowing Vite-served assets when Sec-Fetch-Dest is absent.

Changes:

  • Compute the matched Nitro route once (nitroRouteMatch) and derive whether the match is the root wildcard route (/**).
  • Narrow the asset-extension guard so it only blocks Nitro when the matched route is exactly the root wildcard (/**).
  • Keep existing Sec-Fetch-Dest / Accept based “treat as asset” behavior, but allow explicit prefixed Nitro routes to win.

Comment thread src/build/vite/dev.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
test/vite/baseurl-dotted-param-fixture/routes/[...path].ts (1)

1-1: ⚡ Quick win

Consider using proper event typing and safer parameter access.

The any type for event bypasses type checking, and the non-null assertion on params could cause runtime errors if the parameter is unexpectedly undefined. Consider importing and using the proper Nitro event type.

♻️ Proposed type-safe alternative
-export default (event: any) => "root-wildcard:" + event.context.params!.path;
+import type { EventHandler } from "h3";
+
+export default ((event) => {
+  const path = event.context.params?.path ?? "";
+  return `root-wildcard:${path}`;
+}) satisfies EventHandler;
🤖 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/baseurl-dotted-param-fixture/routes/`[...path].ts at line 1,
Replace the untyped handler and unsafe non-null assertion by importing and using
the proper Nitro event type (e.g., NitroEvent) for the default export's
parameter and accessing params safely; update the default export signature from
(event: any) to (event: NitroEvent), add the NitroEvent import, and read the
path using optional chaining and a safe fallback (e.g.,
event.context?.params?.path ?? '') when constructing the "root-wildcard:" return
value so you avoid runtime errors if params is undefined.
test/vite/baseurl-dotted-param.test.ts (1)

72-81: 💤 Low value

Consider strengthening the negative assertion with additional positive checks.

The test correctly validates that the root wildcard doesn't intercept the request, but the negative assertion doesn't confirm what should happen. While the comment acknowledges Vite's response may vary, you could add a positive check (e.g., status code or content-type) to make the test more robust and self-documenting.

📋 Optional enhancement
   const response = await fetch(`${serverURL}/subdir/entry-client.ts`, {
     headers: { accept: "*/*" },
     redirect: "manual",
   });
+  // Vite should handle this request - validate it's not the root wildcard
   expect(await response.text()).not.toBe("root-wildcard:entry-client.ts");
+  // Additionally confirm Vite attempted to serve it (200 or 404 from Vite, not Nitro)
+  expect(response.status).toBeGreaterThanOrEqual(200);
+  expect(response.status).toBeLessThan(500);
 });
🤖 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/baseurl-dotted-param.test.ts` around lines 72 - 81, The test
"root-level wildcards do not swallow Vite assets when sec-fetch-dest is absent"
currently only asserts the body is not "root-wildcard:entry-client.ts"; enhance
it by adding a positive assertion about the response shape: after calling
fetch(serverURL + '/subdir/entry-client.ts') inspect response.status and
response.headers (e.g., assert response.status is one of the expected values
such as 200 or 404) and assert response.headers.get('content-type') is present
and contains either "text/html" or "javascript" so the test documents what Vite
actually returned in addition to the negative check.
🤖 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.

Nitpick comments:
In `@test/vite/baseurl-dotted-param-fixture/routes/`[...path].ts:
- Line 1: Replace the untyped handler and unsafe non-null assertion by importing
and using the proper Nitro event type (e.g., NitroEvent) for the default
export's parameter and accessing params safely; update the default export
signature from (event: any) to (event: NitroEvent), add the NitroEvent import,
and read the path using optional chaining and a safe fallback (e.g.,
event.context?.params?.path ?? '') when constructing the "root-wildcard:" return
value so you avoid runtime errors if params is undefined.

In `@test/vite/baseurl-dotted-param.test.ts`:
- Around line 72-81: The test "root-level wildcards do not swallow Vite assets
when sec-fetch-dest is absent" currently only asserts the body is not
"root-wildcard:entry-client.ts"; enhance it by adding a positive assertion about
the response shape: after calling fetch(serverURL + '/subdir/entry-client.ts')
inspect response.status and response.headers (e.g., assert response.status is
one of the expected values such as 200 or 404) and assert
response.headers.get('content-type') is present and contains either "text/html"
or "javascript" so the test documents what Vite actually returned in addition to
the negative check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c5bdc22f-d868-43f4-9dee-5d8087bb4c0a

📥 Commits

Reviewing files that changed from the base of the PR and between 4c2c551 and 49ffced.

📒 Files selected for processing (3)
  • src/build/vite/dev.ts
  • test/vite/baseurl-dotted-param-fixture/routes/[...path].ts
  • test/vite/baseurl-dotted-param.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/build/vite/dev.ts

@harshagarwalnyu harshagarwalnyu force-pushed the fix/dev-middleware-asset-ext-swallows-routes branch from 789cf7c to adf0198 Compare May 15, 2026 13:31
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/vite/baseurl-dotted-param.test.ts (1)

60-63: ⚡ Quick win

Remove inline explanatory comments and encode intent in test names/assertions.

These comments restate behavior already captured by descriptive test titles and assertions; please drop them to match repo style.

As per coding guidelines, "Do not add comments explaining what the line does unless prompted".

Also applies to: 79-79, 90-90, 97-97

🤖 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/baseurl-dotted-param.test.ts` around lines 60 - 63, Remove the
inline explanatory comment block starting with "Browsers omit Sec-Fetch-* on
plain-HTTP non-loopback origins..." and similar comment blocks (the ones
mentioning "Specific prefixed routes..." and the other occurrences referencing
Sec-Fetch and prefixed routes), and instead make the test names/assertions
express that behavior directly: rename the affected tests to state the expected
behavior (e.g., "prefixed /api/proxy routes should reach Nitro even with asset
extension and Sec-Fetch present" or equivalent) and strengthen the assertions in
those test cases to validate the exact intent; delete the comment lines and
update the surrounding test titles and expect(...) messages so the behavior is
encoded in the test names/assertions rather than comments.
🤖 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.

Nitpick comments:
In `@test/vite/baseurl-dotted-param.test.ts`:
- Around line 60-63: Remove the inline explanatory comment block starting with
"Browsers omit Sec-Fetch-* on plain-HTTP non-loopback origins..." and similar
comment blocks (the ones mentioning "Specific prefixed routes..." and the other
occurrences referencing Sec-Fetch and prefixed routes), and instead make the
test names/assertions express that behavior directly: rename the affected tests
to state the expected behavior (e.g., "prefixed /api/proxy routes should reach
Nitro even with asset extension and Sec-Fetch present" or equivalent) and
strengthen the assertions in those test cases to validate the exact intent;
delete the comment lines and update the surrounding test titles and expect(...)
messages so the behavior is encoded in the test names/assertions rather than
comments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ac07c5bb-86b0-416e-8009-f928958108c5

📥 Commits

Reviewing files that changed from the base of the PR and between 9f94c07 and adf0198.

📒 Files selected for processing (1)
  • test/vite/baseurl-dotted-param.test.ts

@harshagarwalnyu
Copy link
Copy Markdown
Author

harshagarwalnyu commented May 18, 2026

@coderabbitai review:

  • Removed inline comments throughout the test file
  • Fixed handler typing (satisfies EventHandler)
  • Moved the two stray tests (prefixed splat routes win... and root-level wildcards do not swallow...) inside the describe block — they were accidentally at top-level scope, meaning they had no closure access to serverURL or server. This was a real correctness issue, not just a style nit.

Asset-extensioned URLs (e.g. /api/photos/12345.jpg) were routed to Vite's
static middleware instead of the matching Nitro handler. The ASSET_EXT_RE guard
in nitroDevMiddlewarePre applied unconditionally to any matched route, including
specific prefixed catch-alls like /api/photos/**.

The guard was introduced (nitrojs#4234) to prevent a root-level renderer or user
catch-all (/**) from swallowing Vite's own <script>/<link> serves on plain-HTTP
non-loopback origins where Sec-Fetch-Dest is absent.

Fix: narrow the guard so the asset-extension filter only applies when the matched
Nitro route pattern is /** or /**:param (named root catch-all). Specific routes
(/api/**, /api/photos/**) are never registered on paths Vite owns and should
always win — restoring the behaviour from nitro@3.0.1-alpha.2.

When the matched route's pattern is unreadable, default to false (let the route
win) rather than falling back to the asset-extension guard.

Closes nitrojs#4252
@harshagarwalnyu harshagarwalnyu force-pushed the fix/dev-middleware-asset-ext-swallows-routes branch from 3a48bb9 to bbea608 Compare May 18, 2026 03:12
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the PR details and diff
gh pr view 4266 --repo nitrojs/nitro --json title,state,body,files

# Get the diff of changed files
gh pr diff 4266 --repo nitrojs/nitro

Length of output: 8441


🏁 Script executed:

#!/bin/bash
# Check the type/return value of nitro.routing.routes.match in the codebase
rg -n "routing\.routes\.match\|RoutingRouter\|routes\.match" src/ --type=ts -A3 -B3

# Check what .route property looks like on matched results
rg -n "\.route\b" src/build/vite/dev.ts -A2 -B2

# Check if routes.match could return an array
rg -n "match\(" src/core/router.ts -A5 -B2 2>/dev/null || fd -e ts router src/ --exec grep -n "match(" {}

# Check what matchedRoutePattern could look like in practice
rg -rn "\"\/\*\*\"" src/ --type=ts -C2

Length of output: 578


🏁 Script executed:

#!/bin/bash
# Find the routing module
fd -e ts -e js "router" src/ --exec echo {}

# Look at how routing is set up in nitro
rg -n "routing" src/ --type=ts -l

# Check nitroRouteMatch type context
rg -n "routes\.match" src/ --type=ts -A5 -B5

# Check what radix3 or h3 router match returns
rg -rn "match" src/core/ --type=ts -A3 -B3 | head -80

# Check if there's a RouteMatcher type  
rg -rn "RouteMatcher\|RouterMatch\|matchedRoute" src/ --type=ts -A3 | head -60

Length of output: 1988


🏁 Script executed:

#!/bin/bash
cat src/routing.ts

echo "---TYPES---"
rg -n "routing" src/types/nitro.ts -A5 -B2

Length of output: 6936


@harshagarwalnyu: Thanks for the detailed summary of changes. I've pulled the full diff and traced the routing types through to src/routing.ts — here's the complete review:


✅ User-stated changes verified

  1. Inline comments removed — all three test-file comments (#4241, Browsers omit Sec-Fetch-*, and The extension extraction) are gone from the diff. ✅
  2. satisfies EventHandler typing — the new fixture file correctly uses satisfies EventHandler for type-safe inference. ✅
  3. Stray tests moved inside describeprefixed splat routes win... and the two root-level wildcards... tests are properly indented inside the describe block, giving them closure access to serverURL and server. This was indeed a correctness bug, not just style. ✅

Core logic in src/build/vite/dev.ts

The Array.isArray(nitroRouteMatch) check is valid and necessary, not dead code. Router<T>.match() has this signature:

match(method: string, path: string): undefined | T

where T = MaybeArray<NitroEventHandler & { _importHash: string }>. When mergeCatchAll() runs (triggered by routes._update(..., { merge: true })), it collapses multiple /** handlers into a single node whose .data is an array — so nitroRouteMatch genuinely can be either a single object or an array. The pattern extraction is correct for both cases:

  • Array (merged /**): nitroRouteMatch[0]?.route === "/**"isRootWildcard = true → asset guard applies ✅
  • Single object (specific route, e.g. /api/photos/**): .route === "/api/photos/**"isRootWildcard = false → Nitro always wins ✅

The isRootWildcard pattern covers "/**" (plain catch-all) and "/**:" prefix (named wildcard params like /**:path). The updated nitroWins boolean:

const nitroWins = isNitroRoute && (!isRootWildcard || !(isAssetByExt && treatAsAsset));

is logically correct — specific routes are unconditionally admitted to Nitro; root wildcards remain guarded by the #4234 asset-extension heuristic.


Test coverage

The three new tests exercise the matrix cleanly:

Test fetchDest values Expected
Prefixed splat wins (/subdir/api/proxy/entry-client.ts) script, style, image, absent 200 from Nitro handler
Root wildcard suppressed (/subdir/entry-client.ts) script, style, image, absent 404 (Vite, no asset file)
Root wildcard positive (/subdir/some-page) absent 200 root-wildcard:some-page

The undefined fetchDest case in the prefixed splat test correctly validates the #4252 regression — the exact scenario where Sec-Fetch-Dest is absent on a plain-HTTP non-loopback origin.


The changes are well-scoped, the logic is sound, and the test coverage properly guards both the new fix (#4252) and the preserved behavior (#4234).

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@harshagarwalnyu
Copy link
Copy Markdown
Author

Squashed all prior commits into one (bbea608). The comment referencing commit 3a48bb9 above is now part of that single commit — no logic changes beyond the squash.

@harshagarwalnyu
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@pi0
Copy link
Copy Markdown
Member

pi0 commented May 19, 2026

Thanks a lot for this fix and the clear analysis ❤️ I went with an alternative approach in #4272 that builds on the same root-vs-explicit distinction — it also covers the named root catch-all (/**:slug) case you raised here 🙏

@pi0 pi0 closed this May 19, 2026
@harshagarwalnyu harshagarwalnyu deleted the fix/dev-middleware-asset-ext-swallows-routes branch May 21, 2026 00:55
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.

Vite dev middleware swallows asset-extensioned URLs

3 participants