Skip to content

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

Open
harshagarwalnyu wants to merge 3 commits into
nitrojs:mainfrom
harshagarwalnyu:fix/dev-middleware-asset-ext-swallows-routes
Open

fix(dev): route specific Nitro paths even when URL has asset extension#4266
harshagarwalnyu wants to merge 3 commits 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

Asset-extensioned URLs (e.g. /api/photos/12345.jpg) were being routed
to Vite's static middleware instead of the matching Nitro handler because
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.

Narrow the guard: only apply the asset-extension filter when the matched
Nitro route pattern is the root wildcard /**. Specific routes
(e.g. /api/**, /api/photos/**) are never registered on paths Vite owns,
so they should always win — preserving the original behaviour from
nitro@3.0.1-alpha.2.

Fixes nitrojs#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

📝 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 issue being fixed: routing Nitro paths with asset extensions.
Description check ✅ Passed The description provides clear context about the regression, root cause, the fix strategy, and a comprehensive test plan directly related to the changeset.
Linked Issues check ✅ Passed The PR fully addresses #4252 by modifying the asset-extension guard to only apply to root wildcard routes (/**), allowing specific Nitro routes to take precedence and preventing req._nitroHandled poisoning when matching routes exist.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the issue: core middleware logic refinement in dev.ts, test fixture handler, and expanded test coverage for the specific routing precedence problem.
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

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

2 participants