skip to content

Serving Files and Assets

http.FileServer and ServeContent give you Range requests and If-Modified-Since for free, but the root you hand them decides what a client can reach. Interviewers ask about traversal and listings.

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

questions

4

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

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

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