skip to content

Handlers and Routing

The dispatch-and-answer half of net/http: matching a request to an http.Handler, writing a status and bytes back, serving files, and the request context that dies when the client does.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

22

Why is http.FileServer usually wrapped in http.StripPrefix when mounted under /static/?

level: juniorimportance: must knowfreq 78%

answer

  1. the handler never learns where you mounted it
  2. URL path is joined onto the root
  3. assets/static/app.css instead of assets/app.css
  4. rewrite the path before delegating

basics

~20 s

http.FileServer resolves the entire request path against its root directory, so a request for /static/app.css looks for static/app.css inside that root. http.StripPrefix removes the /static/ mount prefix first, so the file server looks up app.css.

solid answer

~40 s

`http.FileServer(root)` takes the request's URL path verbatim and joins it onto `root`, which is normally an `http.Dir("assets")` or an `http.FS(...)` value. If you register it at the subtree pattern `/static/`, a request for `/static/app.css` arrives with that full path still attached, so the handler tries to open `assets/static/app.css` and returns 404. `http.StripPrefix("/static/", h)` rewrites the request's path to `/app.css` before delegating, which is what makes the mount point and the on-disk layout independent. Two details worth knowing: if the incoming path does not actually begin with the prefix, `StripPrefix` replies 404 rather than passing it through; and if you mount the file server at the root `/`, no stripping is needed because the URL path and the file path already line up.

code

go · 7 lines
go
mux := http.NewServeMux()

// Files live in assets/app.css, assets/logo.svg, ...
fs := http.FileServer(http.Dir("assets"))

// Without StripPrefix this would open assets/static/app.css and 404.
mux.Handle("/static/", http.StripPrefix("/static/", fs))

go deeper

for a junior

Be ready to write the one-line registration from memory: mount pattern, http.StripPrefix with the same string, http.FileServer over http.Dir. Know that a 404 on an asset that is definitely on disk usually means the prefix was not stripped.

for a middle

Explain the mechanics: the mux matches a subtree but leaves r.URL.Path untouched, so the file server joins the full path onto its root. Mention that StripPrefix rewrites both Path and RawPath and answers 404 on a mismatch.

for a senior

Show judgment about the boundary: whether the asset tree should be browsable, whether the URL space should be free to change without moving directories, and how you would catch a prefix mismatch in a test rather than in production.

for a principal

Frame the mount prefix as a contract other teams and CDNs cache against. Changing /static/ later invalidates caches and breaks hard-coded links, so it is worth fixing the URL space early and letting the on-disk layout float behind StripPrefix.

## What `http.FileServer` actually does `http.FileServer` has the signature `func FileServer(root http.FileSystem) http.Handler`. It returns a handler that, for each request, takes `r.URL.Path`, cleans it, and opens that path **relative to `root`**. It does not know or care where in your routing table you mounted it — it only sees the path on the request. `root` is an `http.FileSystem`, an interface with a single method `Open(name string) (http.File, error)`. The two implementations you use in practice are: - `http.Dir("assets")` — a string type that serves files from a directory on the local filesystem. - `http.FS(fsys)` — an adapter, added in Go 1.16, that turns any `fs.FS` (an `embed.FS`, an `os.DirFS`, a zip filesystem) into an `http.FileSystem`. ## Why the prefix is a problem Suppose your files live in `assets/` and you register the handler at the subtree pattern `/static/`: ```go mux.Handle("/static/", http.FileServer(http.Dir("assets"))) ``` A browser asks for `GET /static/app.css`. The mux matches the subtree and calls the handler, but it does **not** modify the request — `r.URL.Path` is still `/static/app.css`. The file server therefore opens `assets/static/app.css`, which does not exist, and answers 404. Everything looks correctly wired and nothing works. There are exactly two ways out. Either you name the directory on disk `assets/static/` so the paths genuinely line up, which couples your URL space to your directory layout forever, or you strip the mount prefix. ## `http.StripPrefix` `func StripPrefix(prefix string, h http.Handler) http.Handler` returns a handler that removes `prefix` from the front of the request path (both `r.URL.Path` and `r.URL.RawPath`, so percent-encoded paths stay consistent) and then calls `h` with a shallow copy of the request carrying the shortened URL. The correct wiring is: ```go mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("assets")))) ``` Now `/static/app.css` becomes `/app.css` before the file server sees it, and it opens `assets/app.css`. Two behaviours catch people out: 1. **Non-matching paths get 404, not a pass-through.** If the wrapped request's path does not begin with `prefix`, `StripPrefix` replies with `404 Not Found` itself. This is almost always what you want, but it means a mismatch between the pattern you registered and the prefix you passed produces a silent, total 404 rather than an obvious error. 2. **The prefix strings must agree.** Registering at `/static/` while stripping `/assets/` compiles fine and 404s everything. Keeping the two literals adjacent on one line, as above, is the cheap defence. ## When you do *not* need it If the file server is mounted at the root — a single-binary site that serves `index.html`, `app.js` and the rest straight off `/` — the URL path and the file path already match and `StripPrefix` would be wrong. Likewise, if you serve one specific file from a handler you do not use `FileServer` at all; you call `http.ServeFile(w, r, name)`. ## Directory behaviour worth knowing `http.FileServer` serves directories too. For a request that resolves to a directory it looks for `index.html` inside it and serves that; failing that it generates a plain HTML directory listing. If you do not want your asset tree browsable, that is a deliberate decision to make — wrap the file system so directory opens fail, or serve individual files with `http.ServeFile`. It also normalises URLs: a request ending in `/index.html` is redirected to `./`, and a directory requested without a trailing slash is redirected to add one. ## The shape to remember Mount point, strip the same string, hand the file server a root that contains the files directly. When a static asset 404s and the file is definitely on disk, the first thing to check is whether the mount prefix is still glued to the path the file server received.

  • What does http.StripPrefix do if the request path does not start with the prefix you gave it?
    It replies `404 Not Found` itself and never calls the wrapped handler. That is why a typo — registering at `/static/` but stripping `/assets/` — produces a total, silent 404 for every asset rather than a startup error.
  • When would you deliberately omit http.StripPrefix?
    When the file server is mounted at the root `/`, because the URL path and the path inside the root already line up. Stripping in that case would chop a real path segment. It is also unnecessary if you shaped the directory tree to mirror the URL space, though that couples the two permanently.
  • What does http.FileServer do when the resolved path is a directory?
    It serves `index.html` from that directory if one exists; otherwise it generates an HTML directory listing. It also redirects a directory requested without a trailing slash to add one, and redirects `.../index.html` to `./`. If you do not want the tree browsable, that listing behaviour is something you have to actively suppress.

saying these in an interview costs you the question

  • Thinks the ServeMux rewrites the path before calling the handler
  • Believes StripPrefix passes non-matching paths through unchanged
  • Adds StripPrefix to a file server mounted at the root
  • Registers /static/ but strips /assets/ and blames the file system
  • Thinks http.Dir opens a file relative to the URL host
open as a page

In a Go HTTP handler, what does r.Context() return and what cancels it?

level: juniorimportance: must knowfreq 72%

basics

~20 s

r.Context() returns a context.Context scoped to that one HTTP request. Go's net/http server cancels it when the client's connection goes away and, unconditionally, when your handler returns from ServeHTTP. Pass it to every downstream call.

open as a page

In a Go http.Handler, why must w.Header().Set calls come before WriteHeader or Write?

level: juniorimportance: must knowfreq 72%

basics

~20 s

The header map is only copied onto the wire when the status line is sent. w.WriteHeader sends it, and the first w.Write sends it implicitly with status 200, so any Header().Set after that point is silently ignored.

open as a page

In Go's http.ServeMux, what does the pattern "GET /items/{id}" match, and how does the handler read id?

level: juniorimportance: must knowfreq 72%

basics

~10 s

It matches GET and HEAD requests whose path is exactly two segments: the literal items followed by any single segment. The handler reads that segment with r.PathValue("id"), which returns it as a string.

open as a page

In Go's net/http, what is the http.Handler interface, and what does http.HandlerFunc do?

level: juniorimportance: must knowfreq 82%

basics

~10 s

http.Handler is a one-method interface: ServeHTTP(http.ResponseWriter, *http.Request). http.HandlerFunc is a function type that has its own ServeHTTP method, so converting a plain function to it turns that function into a Handler.

open as a page

When two http.ServeMux patterns match the same request, how does Go decide which one wins?

level: middleimportance: must knowfreq 55%

basics

~20 s

The more specific pattern wins: the one whose set of matching requests is a strict subset of the other's. Registration order is irrelevant. If neither pattern is a subset of the other they conflict, and registering the second one panics.

open as a page

Your Go handler writes an audit record from a goroutine and the rows stopped appearing, with context.Canceled logged at the write. What happened?

level: seniorimportance: must knowfreq 50%

basics

~20 s

The goroutine was given the request context, which the server cancels as soon as the handler returns, so the audit write is abandoned before it starts. Detach it with context.WithoutCancel and give the detached work its own timeout.

open as a page

How do you serve a //go:embed asset directory over HTTP without the directory name in every URL?

level: middleimportance: should knowfreq 45%

basics

~20 s

An embed.FS keeps the paths exactly as the directive wrote them, so assets/app.css lives at "assets/app.css" inside it. Call fs.Sub(embedded, "assets") to get an fs.FS rooted one level down, then serve that with http.FileServerFS or http.FS.

open as a page

What does http.ServeContent do for you that copying the file bytes into the response does not?

level: middleimportance: should knowfreq 55%

basics

~20 s

http.ServeContent implements byte-range requests, answering 206 Partial Content with a Content-Range header, and conditional requests, answering 304 Not Modified. It also sets Content-Length and a Last-Modified header from the modtime you pass. A raw copy does none of that.

open as a page

In net/http, why is it invalid to read r.Body after ServeHTTP has returned?

level: middleimportance: should knowfreq 45%

basics

~20 s

The server owns the request body and closes it once the handler returns, then may reuse the connection for the next request. Later reads fail or race with unrelated traffic, so copy what you need out of r.Body before returning.

open as a page

How does a Go HTTP handler notice that the client hung up mid-request?

level: middleimportance: should knowfreq 55%

basics

~20 s

Through the request context. Go's server watches the connection while the handler runs and cancels r.Context() when the peer goes away, so a select on ctx.Done(), or a downstream call returning context.Canceled, is how the handler finds out.

open as a page

What do http.Error and http.Redirect write to a response besides the status code?

level: middleimportance: should knowfreq 52%

basics

~10 s

http.Error clears Content-Length, forces Content-Type to text/plain; charset=utf-8, adds X-Content-Type-Options: nosniff, sends the status, then writes the message and a newline. http.Redirect sets Location and, for a GET, writes a small HTML link page.

open as a page

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

level: middleimportance: should knowfreq 40%

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.

open as a page

What do you give up by calling http.ListenAndServe instead of constructing an http.Server?

level: middleimportance: should knowfreq 62%

basics

~20 s

http.ListenAndServe builds an http.Server internally and serves with it. The caller never receives that value, so none of the server's fields can be configured and nothing can reach the server later. A nil handler silently selects DefaultServeMux.

open as a page

Errors from a Go JSON endpoint arrive as 200 and the server logs a superfluous WriteHeader call. Why?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The handler encoded straight to the ResponseWriter, so its first byte sent an implicit 200. The later http.Error could not change a status already on the wire, so net/http logged the superfluous call and appended the message to the partial body.

open as a page

After adding a route to a Go http.ServeMux, requests that used to reach one handler now reach another — how do you diagnose it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The new pattern is more specific, so it wins regardless of registration order, and that overlap is legal rather than an error. Ask the mux: mux.Handler(req) returns the matched pattern. Pin it with a table-driven test.

open as a page

A Go service answers 200 on /debug/pprof/ though its own code registers no such route — why?

level: seniorimportance: should knowfreq 48%

basics

~10 s

An imported package registered those routes on http.DefaultServeMux from its init — net/http/pprof does exactly that when blank-imported — and the server was started with a nil Handler, which means serve DefaultServeMux.

open as a page

How does Go's net/http choose a Content-Type when the handler sets none?

level: middleimportance: nice to knowfreq 35%

basics

~20 s

On the first write, net/http passes up to the first 512 bytes of the body to http.DetectContentType and sends whatever that returns. The sniffer has no JSON signature, so a JSON body is usually labelled text/plain; charset=utf-8.

open as a page

Assets served from an embed.FS come back stale after a deploy — why, and how do you fix it?

level: seniorimportance: nice to knowfreq 35%

basics

~20 s

Embedded files report a zero modification time, so http.ServeContent sends no Last-Modified header, and net/http never invents an ETag. With no validator and no cache directives, browsers apply heuristic freshness and keep the old copy. Fix it with content-hashed filenames or a build-derived ETag.

open as a page

How do you decide which post-response work in a Go service may outlive the request context?

level: principalimportance: nice to knowfreq 32%

basics

~20 s

Split the work by whether losing it is acceptable. Best-effort work may run on a context detached from the request and bounded; anything reconciled later must be written inside the request, because a detached goroutine dies with the process.

open as a page

How do you decide whether http.ServeMux is enough to replace a service's third-party router, and who owns that call?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Inventory what the router does that the standard mux does not, price the migration against a route-behaviour test, and weigh one less dependency against churn with no user-visible gain. The service lead proposes; a platform group can overrule.

open as a page

Should your Go service scaffold ban http.DefaultServeMux and ListenAndServe, and how would you enforce that?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Yes for the default mux, but enforce it as a default rather than a prohibition: the scaffold's bootstrap takes an explicit ServeMux and returns a configured http.Server, so no generated service can reach the global mux. Lint only catches drift.

open as a page