Skip to content

Conversation

@raunak-rpm
Copy link

@raunak-rpm raunak-rpm commented Jan 18, 2026

Summary

This PR fixes two related routing issues:

Issue #1682 - staticPlugin without prefix destroys elysia.mount path

When using staticPlugin without a prefix alongside .mount(), the wildcard route from staticPlugin would take precedence over more specific mount routes.

Issue #1515 - Cannot implement SPA fallback routing with mounted APIs

When using static Response/HTMLBundle on root wildcard routes (e.g., GET /*), Bun's nativeStaticResponse optimization would bypass Elysia's dynamic router, causing mounted API routes to return HTML instead of JSON.

Root Cause Analysis

#1682: The route matching logic used ?? fallback which did not properly compare route specificity when both routes had wildcard captures.

#1515: Static responses are added directly to Bun's static route table, which has highest priority and bypasses Elysia's dynamic router entirely. A root wildcard in the static table matches everything including mount routes.

Solution

For #1682 (src/compose.ts)

  • Changed route lookup from simple ?? fallback to comparing wildcard capture lengths
  • Routes with shorter wildcard captures (more specific) take precedence
  • Uses Object.prototype.hasOwnProperty.call(params, '*') to distinguish:
    • No wildcard (exact match) → highest priority
    • Empty wildcard capture → more specific
    • Longer wildcard capture → less specific

For #1515 (src/adapter/bun/index.ts)

  • During listen(), collect mount prefixes by scanning router.history for routes with hooks.config.mount flag
  • In createStaticRoute(), skip root wildcard paths (/*, /*/, /, ``) when mounts exist
  • This forces root wildcards through the dynamic router where route specificity is properly handled

Why This Approach

Test Coverage

Added comprehensive tests in test/core/spa-fallback.test.ts covering:

  • Root wildcard with single/multiple mounts
  • Registration order independence (wildcard before/after mount)
  • POST requests to mounted routes
  • Specific wildcard paths (/public/*) still work
  • Normal wildcard behavior preserved without mounts
  • Nested mount paths

All 1461 tests pass with no regressions.

Verification

const app = new Elysia()
    .mount('/api', backend)
    .get('/*', staticHtmlResponse)

// Before fix:
// GET /api/users → HTML (bug)

// After fix:
// GET /api/users → JSON ✅
// GET /about → HTML ✅

Fixes #1682
Fixes #1515

When comparing method-specific wildcard routes (e.g., GET /*) with ALL routes
(e.g., mounted routes at ALL /api/*), prefer the more specific route based on
wildcard capture length.

Previously, GET /* would incorrectly override ALL /api/* because the lookup
used nullish coalescing (`??`) which only fell back to ALL when the method-
specific route didn't exist at all.

Now when both routes match:
- Compare wildcard capture lengths (shorter = more specific)
- Method-specific exact matches (no wildcard) still take precedence
- Method-specific routes win when specificity is equal

This ensures that staticPlugin with prefix: "/" doesn't break mounted routes,
while still allowing wildcards to catch unmatched paths.

Fixes elysiajs#1682
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2026

Walkthrough

Reworked route resolution to compare method-specific and ALL-route candidates by inspecting wildcard captures and lengths, selecting the more specific route (preferring method-specific on ties). Added adapter logic to collect mount prefixes and skip conflicting static wildcards. Extended tests for mount/wildcard and SPA-fallback behaviors.

Changes

Cohort / File(s) Summary
Routing Logic Enhancement
src/compose.ts
Replaced prior ALL-aware resolution with an ALL-specific comparison flow: build method-specific and ALL route expressions, introduce temporaries (mr, ar), extract and compare wildcard captures and lengths, and choose the more specific route (method-specific wins on equal specificity). Preserves original path when ALL routes absent.
Adapter mount-prefix handling
src/adapter/bun/index.ts
Collects mount prefixes from app.router.history (routes with mount config and wildcard paths) and skips registering conflicting wildcard static routes to avoid overshadowing mounted routes.
Mount & Wildcard Tests
test/core/mount.test.ts
Adds tests "mount with wildcard routes (issue #1682)" covering mount precedence vs root wildcard, ordering invariance, multiple mounts, method-specific vs ALL tie-breaking, and multi-method mounted behavior (GET/POST/PUT).
SPA Fallback Tests
test/core/spa-fallback.test.ts
Adds SPA-fallback test suite validating mounted backends with root wildcard static responses, multiple mounts, registration ordering, POST handling, specific wildcard paths with mounts, and nested mount path handling.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client as Client
participant Router as Router
participant MR as MethodRoute (mr)
participant AR as AllRoute (ar)
participant Comparator as Comparator
participant Handler as Handler

Client->>Router: request(method, path, headers)
Router->>MR: build method-specific expression
Router->>AR: fetch ALL route candidate
MR->>Comparator: provide wildcard capture(s)
AR->>Comparator: provide wildcard capture(s)
Comparator->>Comparator: compare presence and lengths of '*' captures
Comparator-->>Router: selected route (mr or ar)
Router->>Handler: invoke selected handler
Handler-->>Client: response

(Note: colored rectangles not required for this simple flow.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇
I hopped through routes both wide and small,
I sniffed for stars and counted each fall,
When ties arose I picked the method-first,
Kept mounts safe from a wildcard burst,
A little hop — the router stands tall!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: route specificity for wildcard vs mounted routes (#1682, #1515)' directly and accurately summarizes the main change: fixing how route specificity is determined when wildcard routes and mounted routes both match.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 18, 2026

Open in StackBlitz

npm i https://pkg.pr.new/elysiajs/elysia@1685

commit: 18885db

Copy link
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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/compose.ts`:
- Around line 2343-2366: The specificity check conflates missing wildcard vs an
empty capture by using params['*']||''; update the logic that builds
findDynamicRoute (the mr/ar/mw/aw handling from router.find) to detect presence
of the '*' param explicitly (e.g., check whether params hasOwnProperty '*' or
use ('*' in params) via Object.prototype.hasOwnProperty.call) so you can
distinguish "no wildcard" (undefined) from "empty capture" (''), then choose
route: if mr has no '*' prefer mr, else if ar has no '*' prefer ar, otherwise
compare mw.length vs aw.length to pick the shorter capture (more specific),
keeping existing tie-breaker preferring method-specific (mr).

…city

Use Object.prototype.hasOwnProperty.call to explicitly check for '*' param
presence, properly distinguishing:
- no wildcard (exact match) - highest priority
- empty capture ('') - wildcard matched nothing
- non-empty capture - compare lengths for specificity

This fixes edge cases where the previous ||'' fallback conflated undefined
(no wildcard) with empty string capture.
@raunak-rpm raunak-rpm changed the title fix: route specificity for wildcard vs mounted routes (#1682) fix: route specificity for wildcard vs mounted routes Jan 19, 2026
@raunak-rpm raunak-rpm force-pushed the fix/route-specificity-1682 branch from cb7982b to 57dcc20 Compare January 19, 2026 13:18
…elysiajs#1515)

When using static Response/HTMLBundle on root wildcard routes (e.g., GET /*),
Bun's nativeStaticResponse optimization would add them to Bun's static route
table, which has highest priority and bypasses Elysia's dynamic router.

This caused SPA fallback patterns with mounted APIs to fail:
  app.mount('/api', backend)
     .get('/*', staticHtmlResponse)

The /api/* routes would return HTML instead of API responses because the /*
wildcard in Bun's static table matched everything.

Solution:
- During listen(), collect mount prefixes by scanning router.history for
  routes with hooks.config.mount flag
- In createStaticRoute(), skip root wildcard paths (/* or /*/ or / or empty)
  when mounts exist
- This forces root wildcards through the dynamic router where the route
  specificity fix correctly handles priority

Fixes elysiajs#1515
@raunak-rpm raunak-rpm changed the title fix: route specificity for wildcard vs mounted routes fix: route specificity for wildcard vs mounted routes (#1682, #1515) Jan 19, 2026
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.

staticPlugin without prefix will destroy elysia.mount path Cannot implement SPA fallback routing: HTML bundle serialized as JSON in onError

2 participants