skip to content

What does a trailing slash mean in an http.ServeMux pattern, and what happens to a request without it?

level: middleimportance: should knowfreq 40%

answer

  1. the slash reaches further than you think
  2. an anonymous catch-all wildcard
  3. the dollar sign anchors the end
  4. a slashless request is forwarded, not refused
  5. the forward's status code changed recently

basics

~20 s

A trailing slash makes the pattern match the whole subtree beneath that path, like an anonymous catch-all wildcard. A request for the subtree root without the slash is redirected to the slashed form, unless that bare path is registered too.

solid answer

~50 s

In `ServeMux`, `"/items/"` is a **subtree** pattern: a trailing slash acts as an anonymous `{...}` wildcard, so it matches `/items/`, `/items/42` and `/items/a/b/c`. `"/items"` without the slash matches only that exact path, and `"/items/{$}"` matches only `/items/` and nothing beneath it — `{$}` anchors the pattern to the end of the URL. If only the subtree pattern is registered and a request arrives for the bare `/items`, the mux does not 404: it redirects to `/items/`. Since Go 1.26 that redirect is **307 Temporary Redirect**; before 1.26 it was 301 Moved Permanently, which many clients answer by re-issuing a POST as a GET with no body — a silent data loss the 307 fixes, since 307 preserves method and body. You suppress the redirect entirely by registering `"/items"` yourself. The mux separately cleans paths containing `.`, `..` or repeated slashes and redirects to the cleaned URL.

code

go · 7 lines
go
mux := http.NewServeMux()
mux.HandleFunc("GET /items/", itemsSubtree) // /items/, /items/42, /items/a/b
mux.HandleFunc("GET /files/{path...}", oneFile) // same reach, but named
mux.HandleFunc("GET /{$}", home) // only "/", not every path

// GET /items -> redirect to /items/ (307 since Go 1.26, 301 before)
// register "GET /items" as well to answer it directly instead

go deeper

for a junior

Recall that a pattern ending in a slash covers everything below that path, and that a request for the bare path is redirected to the slashed form rather than refused.

for a middle

Explain the three shapes side by side — /items, /items/ and /items/{$} — and say what each does with a request for /items/42, plus what a trailing slash is equivalent to.

for a senior

Point at the client consequence: before Go 1.26 the redirect was 301 and many clients turn a redirected POST into a body-less GET, so register the bare path explicitly on endpoints that accept writes.

for a principal

Decide the house rule for services you own — slashed or unslashed public paths — since inconsistency costs a redirect round trip per request and leaves cached permanent redirects behind after a fix.

## Three pattern shapes, three meanings Given the resource path `/items`, `ServeMux` gives you three distinct registrations: | pattern | matches | |---|---| | `"/items"` | only the exact path `/items` | | `"/items/"` | `/items/` and everything beneath it: `/items/42`, `/items/a/b` | | `"/items/{$}"` | only the exact path `/items/`, nothing beneath | The documented rule behind the middle row is that **a trailing slash in a pattern path acts as an anonymous `...` wildcard**. `"/items/"` is equivalent in reach to `"/items/{rest...}"`, except that the anonymous form gives you no name to read back with `PathValue`. If you want the remainder of the path as a value — serving a nested key, say — name it: `"GET /files/{path...}"` and `r.PathValue("path")`. The third row is the Go 1.22 addition `{$}`, which matches only the end of the URL. Its most common use is `"/{$}"`: the bare pattern `"/"` is a subtree pattern that matches *every* path, which is why a naive root registration used to swallow all 404s; `"/{$}"` matches the home page alone and lets unregistered paths fall through to the mux's own 404. ## The subtree redirect Suppose only `"/items/"` is registered and a client requests `/items`. Strictly, no pattern matches. Rather than 404, the mux redirects the client to `/items/` — the subtree root with its slash. The behaviour exists so that a subtree handler is reachable by the natural, slash-less URL people type and link. **The status code changed.** Through Go 1.25 the redirect was 301 Moved Permanently. Since **Go 1.26** it is **307 Temporary Redirect**. The change matters for two reasons: 1. **Method and body preservation.** Historically many HTTP clients respond to a 301 on a POST by re-issuing the request as a GET and dropping the body — behaviour that predates the spec's disapproval of it and is still widespread. So a `POST /items` against a service that registered `"/items/"` could arrive at the handler as a body-less GET, and the write would silently vanish. A 307 requires the client to repeat the same method with the same body. 2. **Cacheability.** A 301 is a permanent, cacheable instruction. A browser or intermediary that caches it keeps rewriting `/items` to `/items/` even after you later register `"/items"` explicitly, so the old behaviour could outlive the deploy that fixed it. A 307 carries no such permanence. If you do not want a redirect at all, register the bare path: an explicit `"/items"` (or `"GET /items"`) registration means the request matches directly and the redirect logic never runs. That is the right move for an API where a redirect round trip per request is waste, and the only safe move for endpoints that accept POST from clients you do not control and cannot audit for redirect behaviour. ## Path cleaning is a separate redirect Independently of trailing slashes, the mux sanitises the request path before matching: it strips the port from the Host header, and a path containing `.` segments, `..` segments or repeated slashes is cleaned and the client redirected to the equivalent tidy URL. So `/items//42` and `/items/./42` do not reach your handler directly; the client is bounced to `/items/42` first. This is a security-relevant convenience — it means a handler comparing path text is not handed `..` sequences by the mux — but it also means a request count at the edge can show two requests where a client made one. ## Interaction with precedence Subtree patterns participate in the ordinary specificity rule. `"/items/thumbnails/"` is more specific than `"/items/"`, so both can be registered and the narrower subtree serves its own branch. Likewise `"/items/{id}"` (one segment) and `"/items/"` (the whole subtree) can coexist: the single-segment pattern matches a strict subset of the subtree pattern's requests, so it wins for `/items/42` while the subtree keeps `/items/a/b`. ## What to say in an interview Name the three shapes and their reach, say that a trailing slash is an anonymous catch-all wildcard, and then go one level further than most candidates by mentioning the redirect and its status code — that the standard library will answer a slash-less request with a redirect at all is the part people discover in production, and knowing that the code became 307 in Go 1.26 (from 301) shows you have read a release note rather than only the tutorial.

  • How do you make a ServeMux pattern match one path and nothing beneath it?
    Drop the trailing slash — `"/items"` matches only that path — or, if you want the slashed form specifically, use `"/items/{$}"`. The `{$}` wildcard matches only the end of the URL, which is also how `"/{$}"` registers a home page without swallowing every unmatched path the way `"/"` does.
  • Why was that redirect changed from 301 to 307 in Go 1.26?
    A 301 lets a client re-issue the request as a GET, so a POST to the slash-less path could reach the handler with its method rewritten and its body gone. A 307 requires the same method and body to be repeated. The 301 was also cacheable as permanent, so clients kept following it after the service was fixed.
  • How do you stop the mux redirecting /items to /items/ at all?
    Register the bare path yourself. An explicit `"/items"` (or `"GET /items"`) registration matches the request directly, and the redirect logic only runs when the slash-less path has no registration of its own. For an API that saves a redirect round trip on every call.

A pattern ending in a slash is a whole street rather than a single house number; asking for the street without its slash gets you politely forwarded rather than turned away.

saying these in an interview costs you the question

  • Thinks /items/ matches only the exact path /items/
  • Treats /items and /items/ as the same pattern
  • Assumes the subtree redirect always preserved the method
  • Expects {$} to be a catch-all wildcard
  • Registers / for a home page and wonders where 404s went