diff --git a/http/server.go b/http/server.go index a749c09..f239a64 100644 --- a/http/server.go +++ b/http/server.go @@ -2328,7 +2328,9 @@ type ServeMux struct { type muxEntry struct { h Handler - pattern string + pattern string // full original pattern, e.g. "GET /foo" or "/files/{x...}" + method string // parsed HTTP method, "" if the pattern has no method prefix + path string // path part only (pattern with any leading "METHOD " stripped) } // NewServeMux allocates and returns a new ServeMux. @@ -2374,77 +2376,204 @@ func stripHostPort(h string) string { return host } -// Find a handler on a handler map given a path string. -// Most-specific (longest) pattern wins. -func (mux *ServeMux) match(path string) (h Handler, pattern string) { - // Check for exact match first. - v, ok := mux.m[path] - if ok { - return v.h, v.pattern +// methodMatches reports whether an entry registered with entryMethod (which is +// "" when the pattern had no method prefix) may serve a request whose method is +// reqMethod. An entry with no method matches any method. +func methodMatches(entryMethod, reqMethod string) bool { + return entryMethod == "" || entryMethod == reqMethod +} + +// Find a handler on a handler map given a request method and path string. +// Among all registered patterns whose method is compatible and whose path can +// match, the MOST SPECIFIC one wins, mirroring stdlib net/http precedence: +// segment kinds are compared left-to-right and, at the first differing +// position, the more specific kind wins (literal / {$} > single {id} > +// multi {x...} > trailing-slash subtree). Length is only a final tie-break. +// Only entries whose method is empty or equal to reqMethod are eligible. +func (mux *ServeMux) match(reqMethod, path string) (h Handler, pattern string) { + // Patterns may carry a method prefix, so the map is keyed on the full + // pattern (e.g. "GET /foo"); iterate rather than doing a direct map lookup. + var best *muxEntry + for key := range mux.m { + e := mux.m[key] + if !methodMatches(e.method, reqMethod) { + continue + } + if !entryMatches(e.path, path) { + continue + } + if best == nil || moreSpecificEntry(e, *best) { + ee := e + best = &ee + } + } + if best == nil { + return nil, "" } + return best.h, best.pattern +} - // Check for longest valid match with path placeholders (like /users/{id}) - // First collect all potential matching patterns - var matchingPatterns []string - for registeredPattern := range mux.m { - // Skip patterns ending with "/" as they are handled separately - if registeredPattern[len(registeredPattern)-1] == '/' { - continue +// entryMatches reports whether the entry path can serve lookupPath, covering +// exact, placeholder ({id} / {x...} / {$}) and trailing-slash subtree forms. +func entryMatches(entryPath, lookupPath string) bool { + if entryPath == lookupPath { + return true + } + if entryPath[len(entryPath)-1] == '/' { + // Trailing-slash subtree: matches any path under the prefix. + return strings.HasPrefix(lookupPath, entryPath) + } + if strings.Contains(entryPath, "{") && strings.Contains(entryPath, "}") { + return patternCouldMatch(entryPath, lookupPath) + } + return false +} + +// moreSpecificEntry reports whether entry a should win over entry b for the +// same lookup path. +func moreSpecificEntry(a, b muxEntry) bool { + // A trailing-slash subtree is the least specific (it behaves like a + // multi-segment wildcard tail); any non-subtree pattern beats a subtree. + aSub := a.path[len(a.path)-1] == '/' + bSub := b.path[len(b.path)-1] == '/' + if aSub != bSub { + return bSub // a wins iff b is the (less specific) subtree + } + if aSub && bSub { + // Both subtrees: longer prefix wins, then method-specific. + if len(a.path) != len(b.path) { + return len(a.path) > len(b.path) } + return a.method != "" && b.method == "" + } + // Both exact/placeholder patterns: rank by per-segment specificity. + if morePrecise(a.path, b.path) { + return true + } + if morePrecise(b.path, a.path) { + return false + } + // Equal specificity: a method-specific entry beats a method-less one. + return a.method != "" && b.method == "" +} - // If the pattern contains placeholders and could match the path - if strings.Contains(registeredPattern, "{") && strings.Contains(registeredPattern, "}") { - if patternCouldMatch(registeredPattern, path) { - matchingPatterns = append(matchingPatterns, registeredPattern) - } +// specificityRank maps a segment kind to a precedence weight (higher = more +// specific), mirroring stdlib net/http: a literal is more specific than a +// single-segment capture {id}, which is more specific than a multi-segment +// wildcard {x...}. The {$} end anchor matches a single exact end position and +// ranks with literals. +func specificityRank(kind int) int { + switch kind { + case segLiteral, segEnd: + return 3 + case segCapture: + return 2 + case segMulti: + return 1 + } + return 0 +} + +// morePrecise reports whether pattern a is strictly more specific than b: +// compare segment kinds left-to-right and, at the first position whose kinds +// differ, the more specific kind wins. If all compared kinds tie, the longer +// pattern (more segments / longer literals) wins. +func morePrecise(a, b string) bool { + as := splitSegs(a) + bs := splitSegs(b) + n := len(as) + if len(bs) < n { + n = len(bs) + } + for i := 0; i < n; i++ { + ka, _ := classifySeg(as[i]) + kb, _ := classifySeg(bs[i]) + ra, rb := specificityRank(ka), specificityRank(kb) + if ra != rb { + return ra > rb } } + return len(a) > len(b) +} - // Find the longest matching pattern - if len(matchingPatterns) > 0 { - sort.Slice(matchingPatterns, func(i, j int) bool { - return len(matchingPatterns[i]) > len(matchingPatterns[j]) - }) +// Segment kinds for a pattern segment. +const ( + segLiteral = iota // literal text, must match exactly + segCapture // {id}: matches one non-empty segment, captured by name + segMulti // {rest...}: trailing multi-segment wildcard (last only) + segEnd // {$}: end-of-path anchor (no capture) +) - pattern = matchingPatterns[0] - return mux.m[pattern].h, pattern +// classifySeg classifies a single pattern segment and returns the capture name +// (for segCapture / segMulti; empty otherwise). +func classifySeg(seg string) (kind int, name string) { + if !strings.HasPrefix(seg, "{") || !strings.HasSuffix(seg, "}") { + return segLiteral, "" + } + inner := seg[1 : len(seg)-1] + if inner == "$" { + return segEnd, "" + } + if strings.HasSuffix(inner, "...") { + return segMulti, inner[:len(inner)-len("...")] } + return segCapture, inner +} - // Check for longest valid match. mux.es contains all patterns - // that end in / sorted from longest to shortest. - for _, e := range mux.es { - if strings.HasPrefix(path, e.pattern) { - return e.h, e.pattern - } +// splitSegs splits a path/pattern into segments. Unlike a plain +// strings.Split(strings.Trim(s,"/"),"/") it preserves a single trailing empty +// segment (so "/a/" -> ["a",""] and "/" -> [""]), which the {$} end-anchor and +// the {x...} multi-wildcard rely on. +func splitSegs(s string) []string { + t := strings.TrimPrefix(s, "/") + if t == "" { + return []string{""} } - return nil, "" + return strings.Split(t, "/") } -// patternCouldMatch checks if a pattern with placeholders could match the given path. -// It handles patterns like /users/{id}/orders/{orderId}. +// patternCouldMatch checks if a pattern with placeholders could match the given +// path. It handles single captures (/users/{id}), the trailing multi-segment +// wildcard (/files/{path...}), and the end-of-path anchor (/exact/{$}). func patternCouldMatch(pattern, path string) bool { - patternParts := strings.Split(strings.Trim(pattern, "/"), "/") - pathParts := strings.Split(strings.Trim(path, "/"), "/") + patternParts := splitSegs(pattern) + pathParts := splitSegs(path) + + for i, pp := range patternParts { + kind, _ := classifySeg(pp) + + switch kind { + case segEnd: + // {$} is only valid as the final pattern segment and matches only + // the empty final path segment (a path written like "/exact/"). + if i != len(patternParts)-1 { + return false + } + return i == len(pathParts)-1 && pathParts[i] == "" - // If they have different number of parts, they can't match - if len(patternParts) != len(pathParts) { - return false - } + case segMulti: + // Trailing multi-segment wildcard: must be the final pattern + // segment and consumes all remaining path segments. + if i != len(patternParts)-1 { + return false + } + return i <= len(pathParts)-1 - // Check each part - for i, patternPart := range patternParts { - // If it's a placeholder, it matches anything - if strings.HasPrefix(patternPart, "{") && strings.HasSuffix(patternPart, "}") { - continue - } + case segCapture: + // {id} matches exactly one non-empty segment. + if i >= len(pathParts) || pathParts[i] == "" { + return false + } - // If it's not a placeholder, it must match exactly - if patternPart != pathParts[i] { - return false + default: // literal + if i >= len(pathParts) || pp != pathParts[i] { + return false + } } } - return true + // No multi/anchor consumed the tail: segment counts must match exactly. + return len(patternParts) == len(pathParts) } // redirectToPathSlash determines if the given path needs appending "/" to it. @@ -2514,7 +2643,7 @@ func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { return RedirectHandler(u.String(), StatusMovedPermanently), u.Path } - return mux.handler(r.Host, r.URL.Path) + return mux.handler(r.Method, r.Host, r.URL.Path) } // All other requests have any port stripped and path cleaned @@ -2529,26 +2658,26 @@ func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { } if path != r.URL.Path { - _, pattern = mux.handler(host, path) + _, pattern = mux.handler(r.Method, host, path) u := &url.URL{Path: path, RawQuery: r.URL.RawQuery} return RedirectHandler(u.String(), StatusMovedPermanently), pattern } - return mux.handler(host, r.URL.Path) + return mux.handler(r.Method, host, r.URL.Path) } // handler is the main implementation of Handler. // The path is known to be in canonical form, except for CONNECT methods. -func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { +func (mux *ServeMux) handler(method, host, path string) (h Handler, pattern string) { mux.mu.RLock() defer mux.mu.RUnlock() // Host-specific pattern takes precedence over generic ones if mux.hosts { - h, pattern = mux.match(host + path) + h, pattern = mux.match(method, host+path) } if h == nil { - h, pattern = mux.match(path) + h, pattern = mux.match(method, path) } if h == nil { h, pattern = NotFoundHandler(), "" @@ -2562,24 +2691,33 @@ func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { func extractPathValues(pattern, path string) map[string]string { pathValues := make(map[string]string) - // Split both pattern and path by "/" - patternParts := strings.Split(strings.Trim(pattern, "/"), "/") - pathParts := strings.Split(strings.Trim(path, "/"), "/") - - // If they have different lengths, they can't match (unless the pattern has a wildcard) - if len(patternParts) != len(pathParts) { - return pathValues - } - - // Compare each part - for i, patternPart := range patternParts { - if strings.HasPrefix(patternPart, "{") && strings.HasSuffix(patternPart, "}") { - // This is a placeholder like {id} - paramName := patternPart[1 : len(patternPart)-1] - pathValues[paramName] = pathParts[i] - } else if patternPart != pathParts[i] { - // If a non-placeholder part doesn't match, return empty - return make(map[string]string) + patternParts := splitSegs(pattern) + pathParts := splitSegs(path) + + for i, pp := range patternParts { + kind, name := classifySeg(pp) + + switch kind { + case segEnd: + // {$} is an anchor, not a capture; nothing to store. + return pathValues + case segMulti: + // Trailing wildcard captures the joined remainder (possibly empty). + if i <= len(pathParts)-1 { + pathValues[name] = strings.Join(pathParts[i:], "/") + } else { + pathValues[name] = "" + } + return pathValues + case segCapture: + if i < len(pathParts) { + pathValues[name] = pathParts[i] + } + default: // literal + if i >= len(pathParts) || pp != pathParts[i] { + // If a non-placeholder part doesn't match, return empty. + return make(map[string]string) + } } } @@ -2598,9 +2736,11 @@ func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { } h, pattern := mux.Handler(r) - // Extract path values for patterns that contain placeholders like {id} + // Extract path values for patterns that contain placeholders like {id}. + // Strip any leading "METHOD " so values come from the path part only. if strings.Contains(pattern, "{") && strings.Contains(pattern, "}") { - pathValues := extractPathValues(pattern, r.URL.Path) + _, patternPath := parseMethod(pattern) + pathValues := extractPathValues(patternPath, r.URL.Path) // Set path values in the request for name, value := range pathValues { @@ -2630,21 +2770,54 @@ func (mux *ServeMux) Handle(pattern string, handler Handler) { if mux.m == nil { mux.m = make(map[string]muxEntry) } - e := muxEntry{h: handler, pattern: pattern} + // Parse an optional leading "METHOD " so the rest of the routing logic works + // on the path part only, and so a method prefix never sets mux.hosts. + method, patternPath := parseMethod(pattern) + // A pattern that strips to an empty path (e.g. "GET ") is invalid; stdlib + // rejects it. Guard before indexing patternPath[len-1] / patternPath[0]. + if patternPath == "" { + panic("http: invalid pattern") + } + e := muxEntry{h: handler, pattern: pattern, method: method, path: patternPath} mux.m[pattern] = e - if pattern[len(pattern)-1] == '/' { + if patternPath[len(patternPath)-1] == '/' { mux.es = appendSorted(mux.es, e) } - if pattern[0] != '/' { + if patternPath[0] != '/' { mux.hosts = true } } +// parseMethod splits an optional leading "METHOD " off a pattern. A method is a +// token before the first space consisting solely of upper-case ASCII letters. +// If there is no such prefix it returns ("", pattern). +func parseMethod(pattern string) (method, path string) { + if i := strings.IndexByte(pattern, ' '); i >= 0 { + cand := pattern[:i] + if isMethodToken(cand) { + return cand, strings.TrimLeft(pattern[i+1:], " ") + } + } + return "", pattern +} + +func isMethodToken(s string) bool { + if s == "" { + return false + } + for i := 0; i < len(s); i++ { + if s[i] < 'A' || s[i] > 'Z' { + return false + } + } + return true +} + func appendSorted(es []muxEntry, e muxEntry) []muxEntry { n := len(es) i := sort.Search(n, func(i int) bool { - return len(es[i].pattern) < len(e.pattern) + return len(es[i].path) < len(e.path) }) if i == n { return append(es, e) diff --git a/http/server_test.go b/http/server_test.go new file mode 100644 index 0000000..aa10772 --- /dev/null +++ b/http/server_test.go @@ -0,0 +1,259 @@ +// TINYGO: Tests for the custom ServeMux routing logic. +// +// These cover the three behaviours that previously diverged from the standard +// library's net/http.ServeMux (see issue #55): +// +// 1. {name...} multi-segment trailing wildcards, +// 2. the {$} end-of-path anchor, and +// 3. method-prefixed patterns ("GET /path"). +// +// Plus precedence and no-regression cases for exact / {id} / subtree / host +// patterns. Expected outcomes mirror net/http.ServeMux on Go 1.26. +// +// NOTE: this package has no module-root go.mod (it imports TinyGo internals), +// so these tests are not run on the host; they document and lock the intended +// behaviour and run under TinyGo's test harness. They are written to use only +// the package's own exported/unexported API and a tiny in-test ResponseWriter +// so there is no dependency on net/http/httptest (which would import this +// package and create a cycle). + +package http + +import ( + "net/url" + "testing" +) + +// testResponseWriter is a minimal ResponseWriter; the routing tests never +// inspect the response body, they only need ServeHTTP to be callable so that +// path values get populated on the request. +type testResponseWriter struct { + header Header + code int +} + +func newTestResponseWriter() *testResponseWriter { + return &testResponseWriter{header: make(Header)} +} + +func (w *testResponseWriter) Header() Header { return w.header } +func (w *testResponseWriter) Write(b []byte) (int, error) { return len(b), nil } +func (w *testResponseWriter) WriteHeader(code int) { w.code = code } + +// newTestRequest builds a server-side *Request with just the fields the mux +// reads: Method, Host and URL.Path. RequestURI is set so ServeHTTP's "*" guard +// is not triggered. +func newTestRequest(method, host, path string) *Request { + if method == "" { + method = MethodGet + } + if host == "" { + host = "example.com" + } + return &Request{ + Method: method, + Host: host, + URL: &url.URL{Path: path}, + RequestURI: path, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + } +} + +// route registers patterns on a fresh mux, dispatches a request and returns the +// matched pattern ("" means 404) together with any path values the matched +// pattern exposed via r.PathValue. +func route(t *testing.T, patterns []string, method, host, path string, valNames []string) (pattern string, vals map[string]string) { + t.Helper() + mux := NewServeMux() + for _, p := range patterns { + mux.HandleFunc(p, func(ResponseWriter, *Request) {}) + } + + r := newTestRequest(method, host, path) + _, pattern = mux.Handler(r) + + vals = map[string]string{} + if len(valNames) > 0 { + // Drive the full ServeHTTP path so SetPathValue runs, then read back. + captured := newTestRequest(method, host, path) + valMux := NewServeMux() + for _, p := range patterns { + valMux.HandleFunc(p, func(_ ResponseWriter, req *Request) { + captured = req + }) + } + valMux.ServeHTTP(newTestResponseWriter(), captured) + for _, n := range valNames { + vals[n] = captured.PathValue(n) + } + } + return pattern, vals +} + +func TestServeMux_Routing(t *testing.T) { + cases := []struct { + name string + patterns []string + method string + host string + path string + // wantPattern is the registered pattern expected to match; "" => 404. + wantPattern string + // wantVals maps a wildcard name to its expected captured value. + wantVals map[string]string + }{ + // --- exact match (no regression) --- + {name: "exact-hit", patterns: []string{"/foo"}, path: "/foo", wantPattern: "/foo"}, + {name: "exact-miss", patterns: []string{"/foo"}, path: "/bar", wantPattern: ""}, + {name: "root-subtree-catches-all", patterns: []string{"/"}, path: "/anything", wantPattern: "/"}, + + // --- single {id} (no regression) --- + {name: "id-hit", patterns: []string{"/users/{id}"}, path: "/users/42", + wantPattern: "/users/{id}", wantVals: map[string]string{"id": "42"}}, + {name: "id-too-many-segments-404", patterns: []string{"/users/{id}"}, path: "/users/42/x", wantPattern: ""}, + {name: "id-two", patterns: []string{"/users/{id}/orders/{oid}"}, path: "/users/7/orders/9", + wantPattern: "/users/{id}/orders/{oid}", wantVals: map[string]string{"id": "7", "oid": "9"}}, + {name: "id-empty-segment-404", patterns: []string{"/users/{id}"}, path: "/users/", wantPattern: ""}, + + // --- trailing-slash subtree (no regression) --- + {name: "subtree-hit", patterns: []string{"/static/"}, path: "/static/a/b", wantPattern: "/static/"}, + {name: "subtree-exact-prefix", patterns: []string{"/static/"}, path: "/static/", wantPattern: "/static/"}, + + // --- Bug 1: {name...} multi-segment wildcard --- + {name: "multi-many", patterns: []string{"/files/{path...}"}, path: "/files/a/b/c", + wantPattern: "/files/{path...}", wantVals: map[string]string{"path": "a/b/c"}}, + {name: "multi-one", patterns: []string{"/files/{path...}"}, path: "/files/a", + wantPattern: "/files/{path...}", wantVals: map[string]string{"path": "a"}}, + {name: "multi-empty", patterns: []string{"/files/{path...}"}, path: "/files/", + wantPattern: "/files/{path...}", wantVals: map[string]string{"path": ""}}, + {name: "multi-wrong-prefix-404", patterns: []string{"/files/{path...}"}, path: "/other/x", wantPattern: ""}, + + // --- Bug 1 precedence: literal > {id} > {x...} at same end-path --- + {name: "multi-vs-literal-literal-wins", patterns: []string{"/files/{path...}", "/files/special"}, + path: "/files/special", wantPattern: "/files/special"}, + {name: "id-beats-multi-on-one-seg", patterns: []string{"/a/{id}", "/a/{x...}"}, path: "/a/one", + wantPattern: "/a/{id}", wantVals: map[string]string{"id": "one"}}, + {name: "multi-wins-on-two-seg", patterns: []string{"/a/{id}", "/a/{x...}"}, path: "/a/one/two", + wantPattern: "/a/{x...}", wantVals: map[string]string{"x": "one/two"}}, + + // --- Bug 2: {$} end-of-path anchor --- + {name: "anchor-hit", patterns: []string{"/exact/{$}"}, path: "/exact/", wantPattern: "/exact/{$}"}, + {name: "anchor-reject-sub-404", patterns: []string{"/exact/{$}"}, path: "/exact/sub", wantPattern: ""}, + {name: "anchor-no-bogus-value", patterns: []string{"/exact/{$}"}, path: "/exact/", + wantPattern: "/exact/{$}", wantVals: map[string]string{"$": ""}}, + {name: "root-anchor-hit", patterns: []string{"/{$}"}, path: "/", wantPattern: "/{$}"}, + {name: "root-anchor-reject-404", patterns: []string{"/{$}"}, path: "/x", wantPattern: ""}, + + // --- Bug 2 precedence: {$} beats both subtree and {x...} at "/d/" --- + {name: "anchor-beats-subtree-at-end", patterns: []string{"/d/{$}", "/d/"}, path: "/d/", wantPattern: "/d/{$}"}, + {name: "subtree-still-catches-deep", patterns: []string{"/d/{$}", "/d/"}, path: "/d/deep", wantPattern: "/d/"}, + {name: "anchor-beats-multi-at-end", patterns: []string{"/d/{$}", "/d/{x...}"}, path: "/d/", wantPattern: "/d/{$}"}, + {name: "multi-catches-deep-over-anchor", patterns: []string{"/d/{$}", "/d/{x...}"}, path: "/d/z", + wantPattern: "/d/{x...}", wantVals: map[string]string{"x": "z"}}, + + // --- Bug 3: method-prefixed patterns --- + {name: "method-get-hit", patterns: []string{"GET /m"}, method: "GET", path: "/m", wantPattern: "GET /m"}, + {name: "method-get-on-post-404", patterns: []string{"GET /m"}, method: "POST", path: "/m", wantPattern: ""}, + {name: "method-pick-post", patterns: []string{"GET /m", "POST /m"}, method: "POST", path: "/m", wantPattern: "POST /m"}, + {name: "method-and-plain-falls-through", patterns: []string{"GET /m", "/m"}, method: "DELETE", path: "/m", wantPattern: "/m"}, + {name: "method-id", patterns: []string{"GET /u/{id}"}, method: "GET", path: "/u/5", + wantPattern: "GET /u/{id}", wantVals: map[string]string{"id": "5"}}, + {name: "method-multi", patterns: []string{"POST /up/{rest...}"}, method: "POST", path: "/up/a/b", + wantPattern: "POST /up/{rest...}", wantVals: map[string]string{"rest": "a/b"}}, + + // --- Bug 3: a method prefix must NOT enable host routing and corrupt + // other plain routes (the original bug set mux.hosts=true). --- + {name: "method-no-hosts-corruption-plain", patterns: []string{"GET /a", "/p"}, method: "GET", path: "/p", wantPattern: "/p"}, + {name: "method-no-hosts-corruption-method", patterns: []string{"GET /a", "/p"}, method: "GET", path: "/a", wantPattern: "GET /a"}, + + // --- method + specificity interaction --- + {name: "methodless-multi-wins-on-method-miss", patterns: []string{"GET /a/{id}", "/a/{x...}"}, + method: "POST", path: "/a/one", wantPattern: "/a/{x...}", wantVals: map[string]string{"x": "one"}}, + {name: "method-id-wins-on-method-hit", patterns: []string{"GET /a/{id}", "/a/{x...}"}, + method: "GET", path: "/a/one", wantPattern: "GET /a/{id}", wantVals: map[string]string{"id": "one"}}, + + // --- host patterns (no regression) --- + {name: "host-pattern-match", patterns: []string{"example.com/h", "/h"}, host: "example.com", path: "/h", + wantPattern: "example.com/h"}, + {name: "host-pattern-fallthrough", patterns: []string{"example.com/h", "/h"}, host: "other.com", path: "/h", + wantPattern: "/h"}, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + var valNames []string + for n := range c.wantVals { + valNames = append(valNames, n) + } + + gotPattern, gotVals := route(t, c.patterns, c.method, c.host, c.path, valNames) + + if gotPattern != c.wantPattern { + t.Fatalf("pattern: got %q, want %q", gotPattern, c.wantPattern) + } + for n, want := range c.wantVals { + if got := gotVals[n]; got != want { + t.Errorf("PathValue(%q): got %q, want %q", n, got, want) + } + } + }) + } +} + +// TestServeMux_MethodPrefixDoesNotSetHosts asserts the specific corruption from +// bug 3: registering a method-prefixed pattern must not flip mux.hosts, which +// would otherwise force every lookup through host+path first. +func TestServeMux_MethodPrefixDoesNotSetHosts(t *testing.T) { + mux := NewServeMux() + mux.HandleFunc("GET /foo", func(ResponseWriter, *Request) {}) + if mux.hosts { + t.Fatalf("registering %q wrongly set mux.hosts = true", "GET /foo") + } + + // A genuine host pattern still sets it. + mux2 := NewServeMux() + mux2.HandleFunc("example.com/foo", func(ResponseWriter, *Request) {}) + if !mux2.hosts { + t.Fatalf("registering a host pattern should set mux.hosts = true") + } +} + +// TestServeMux_InvalidMethodOnlyPatternPanics asserts that a pattern that +// strips to an empty path (e.g. "GET ") is rejected, matching net/http. +func TestServeMux_InvalidMethodOnlyPatternPanics(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("Handle(%q) should panic on an empty path part", "GET ") + } + }() + mux := NewServeMux() + mux.HandleFunc("GET ", func(ResponseWriter, *Request) {}) +} + +// TestParseMethod checks the leading-method tokenizer used by Handle. +func TestParseMethod(t *testing.T) { + cases := []struct { + in string + wantMethod string + wantPath string + }{ + {"/foo", "", "/foo"}, + {"GET /foo", "GET", "/foo"}, + {"POST /a/{id}", "POST", "/a/{id}"}, + {"example.com/foo", "", "example.com/foo"}, // host, not a method + {"GET /foo", "GET", "/foo"}, // extra spaces trimmed + {"get /foo", "", "get /foo"}, // lower-case is not a method token + {"GET", "", "GET"}, // no space, treated as path + {"GET /a b", "GET", "/a b"}, // only the first space splits + } + for _, c := range cases { + gotMethod, gotPath := parseMethod(c.in) + if gotMethod != c.wantMethod || gotPath != c.wantPath { + t.Errorf("parseMethod(%q) = (%q, %q), want (%q, %q)", + c.in, gotMethod, gotPath, c.wantMethod, c.wantPath) + } + } +}