Skip to content

http: fix ServeMux {wildcard...}, {$}, and method-prefixed patterns#56

Open
ultramcu wants to merge 1 commit into
tinygo-org:mainfrom
ultramcu:fix/servemux-routing
Open

http: fix ServeMux {wildcard...}, {$}, and method-prefixed patterns#56
ultramcu wants to merge 1 commit into
tinygo-org:mainfrom
ultramcu:fix/servemux-routing

Conversation

@ultramcu

@ultramcu ultramcu commented Jun 4, 2026

Copy link
Copy Markdown

Fixes #55.

The hand-rolled ServeMux in http/server.go mishandled three Go-1.22 routing features, each diverging from net/http.ServeMux. Per @deadprogram's note on #55 that the divergence wasn't intentional, this fixes all three.

Bugs fixed

  1. {name...} multi-segment wildcard never matched. mux.Handle("/files/{path...}", h); GET /files/a/b/c returned 404. patternCouldMatch/extractPathValues split on / and required equal segment counts, so a trailing {x...} could only match a single segment. It now consumes the remainder — including the empty remainder (/files/PathValue("path") == ""), matching net/http.

  2. {$} end-anchor treated as an ordinary capture. /exact/{$} over-matched /exact/sub and set a bogus PathValue("$") == "sub". It now matches only the exact /exact/ and is never captured as a value.

  3. Method-prefixed patterns ("GET /foo") didn't route and corrupted host routing. The entry was stored under the full "GET /foo" key (so a bare-path lookup missed it), and because the pattern's first byte wasn't /, mux.hosts was flipped to true, breaking routing for the whole mux. Handle now parses an optional leading method, keys match decisions on the stripped path, and selects entries by method plus net/http-compatible specificity.

Verification

Verified against the real net/http.ServeMux as an oracle (on host) across the three features above, precedence interactions ({id} vs {x...} single/deep, {$} vs a trailing-slash subtree, method vs method-less), and no-regression on exact / {id} / subtree / host patterns. Fail-before/pass-after holds and the winning pattern is order-independent (transitive) and agrees with net/http on every legal registration set tested.

http/server_test.go adds table-driven coverage for all three features plus the precedence and no-regression cases. (Note: it runs under the TinyGo test harness; the package can't be go test-ed standalone on host since there's no module-root go.mod, so verification was done with copied helpers against the net/http oracle.)

Scope notes

  • 404 vs 405 on method mismatch. A POST to a GET-only route returns 404, not net/http's 405 Method Not Allowed + Allow header. TinyGo's ServeMux has no 405/Allow machinery; adding it would mean tracking the method set per path and is left out of scope. The routing decision (match vs no-match) matches net/http.
  • No registration-time conflict detection. Overlapping patterns are resolved by specificity ranking rather than rejected at registration as net/http does (net/http panics on genuinely-ambiguous pairs). This preserves the existing hand-rolled matcher's leniency.
  • Kept minimal. The repo's http/pattern.go (the full upstream matcher) is intentionally left unused; this fix stays within the existing hand-rolled ServeMux rather than rewiring it.
  • Patterns mixing a wildcard with a trailing-slash subtree (e.g. /a/{id}/) remain unsupported, as before (not a regression).

The hand-rolled ServeMux diverged from net/http.ServeMux on three
Go-1.22 routing features:

- {name...} multi-segment wildcards never matched: patternCouldMatch and
  extractPathValues split on "/" and required equal segment counts, so a
  trailing {x...} could only ever match a single segment. It now consumes
  the remainder (including the empty remainder, e.g. /files/ -> path="").

- {$} end-of-path anchors were treated as ordinary captures: /exact/{$}
  over-matched /exact/sub and set a bogus PathValue("$"). It now matches
  only the exact path and is never captured as a value.

- Method-prefixed patterns ("GET /foo") did not route and corrupted host
  routing: the entry was stored under the full key so a bare-path lookup
  missed it, and a non-"/" first byte set mux.hosts=true, breaking every
  other route. Handle now parses an optional leading method, keys match
  decisions on the stripped path, and selects entries by method plus
  net/http-compatible specificity.

Verified against net/http.ServeMux as an oracle (host) across the three
features plus precedence interactions and no-regression on exact, {id},
subtree and host patterns; fail-before/pass-after holds.
@b0ch3nski

Copy link
Copy Markdown

@deadprogram PTAL

@deadprogram

Copy link
Copy Markdown
Member

The following is a lightly edited version of a generated review:

PR #56 Review Assessment

PR: #56
Title: http: fix ServeMux {wildcard...}, {$}, and method-prefixed patterns
Author: @ultramcu
Date reviewed: 2026-07-23

Summary: Fixes three bugs in the hand-rolled ServeMux for Go 1.22+ routing features: {name...} multi-segment wildcards, {$} end-anchor, and method-prefixed patterns ("GET /foo").


Correctness: ✓ Looks sound

The core logic is correct. The segment classification (classifySeg), matching (patternCouldMatch), and precedence (moreSpecificEntry / morePrecise) all produce the right results for the documented cases. The specificityRank ordering (literal/{$} > {id} > {x...} > subtree) correctly mirrors net/http.


Issues Found

1. Dead code: mux.es and appendSorted (minor)

The es []muxEntry field (server.go L2325) is populated in Handle at line 2784 but never read. The old match() iterated mux.es for trailing-slash patterns; the new match() iterates mux.m for everything. The appendSorted function at line 2817 is now dead code. Should be removed or flagged.

2. Performance regression from O(1) → O(n) (acceptable, but worth noting)

The old match did mux.m[path] for an O(1) exact-match fast path. The new match always iterates all entries. On embedded devices with very few routes this is fine, but it's a linear scan per request. A simple optimization: check mux.m[path] first (only for method-less exact patterns) before iterating.

3. shouldRedirectRLocked doesn't account for method-prefixed keys (latent bug)

shouldRedirectRLocked does mux.m[path+"/"] to check if a slash-appended handler exists. For a pattern registered as "GET /foo/", the map key is "GET /foo/", not "/foo/", so the redirect won't fire. This means the /foo/foo/ redirect is silently broken for method-prefixed trailing-slash patterns. The fix is probably to iterate and check e.path instead of doing a direct key lookup. Low severity since it's a pre-existing design gap, but the PR makes it easier to hit.

4. Non-determinism for genuinely ambiguous patterns (documented, acceptable)

Two patterns like /a/{x} and /a/{y} (same structure, different names) matching the same path yield non-deterministic results due to map iteration order. The PR's scope notes explicitly accept this ("no registration-time conflict detection"), so this is fine.


Strengths

  • Well-scoped: Stays within the existing hand-rolled matcher; doesn't attempt to wire in http/pattern.go.
  • Excellent test coverage: 30+ table-driven cases covering all three bugs, precedence interactions, and no-regression for existing features. Tests are self-contained with no external dependencies.
  • parseMethod is robust: Correctly rejects hostnames (contain ./digits), lowercase tokens, and missing-space cases.
  • Comments are thorough: Every helper has clear doc comments explaining its role.
  • {$} is never captured as a value: extractPathValues returns early on segEnd without storing anything — avoids the bogus PathValue("$") bug.

Verdict

Approve with minor feedback. The functional changes are correct and well-tested. The only actionable items are:

  1. Remove dead mux.es / appendSorted code (or leave a // TODO: remove comment if keeping for git-blame continuity).
  2. Optionally note the shouldRedirectRLocked gap for method-prefixed slash patterns as a known limitation or future TODO.

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.

http: ServeMux mishandles {wildcard...}, {$}, and method-prefixed patterns

3 participants