skip to content

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

level: middleimportance: should knowfreq 55%

answer

  1. it must be able to seek, not just read
  2. partial bodies and empty bodies
  3. 206 and 304 are the two shortcuts
  4. the modtime argument feeds one header
  5. a zero timestamp disables half of it

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.

solid answer

~50 s

`http.ServeContent(w, r, name, modtime, content)` takes an `io.ReadSeeker`, and the seeking is the point: it can determine the size and jump to an offset, which is what lets it honour a `Range: bytes=...` header with a `206 Partial Content` response and a `Content-Range` header, or reject an out-of-bounds range with `416`. It also evaluates the request's conditional headers — `If-None-Match`, `If-Modified-Since`, `If-Match`, `If-Unmodified-Since` and `If-Range` — and can short-circuit to `304 Not Modified` with no body at all. It emits `Last-Modified` from the `modtime` argument, unless that value is the zero `time.Time`, in which case it omits the header and skips the modification-time check entirely. It also picks a content type from the extension of `name`. Copying bytes yourself gives you a 200 with the whole body every single time, so resumable downloads, video seeking and client revalidation are all lost.

code

go · 17 lines
go
func serveManual(w http.ResponseWriter, r *http.Request) {
	f, err := os.Open("docs/handbook.pdf")
	if err != nil {
		http.NotFound(w, r)
		return
	}
	defer f.Close()

	st, err := f.Stat()
	if err != nil {
		http.NotFound(w, r)
		return
	}

	// Range, If-Modified-Since, Content-Length and Last-Modified: handled.
	http.ServeContent(w, r, "handbook.pdf", st.ModTime(), f)
}

go deeper

for a junior

Know that Go has a helper for sending a file over HTTP and that you should reach for it instead of copying bytes yourself. Remember the names http.ServeFile and http.ServeContent and that one wraps the other.

for a middle

Explain the mechanics: why the argument is an io.ReadSeeker, what 206 with Content-Range means, what triggers 304, and what the modtime argument controls — including what happens when it is the zero time.

for a senior

Show you can reason about the consequences in production: resumable large downloads, media seeking, revalidation traffic, and the fact that no ETag appears unless your own code sets one before the call.

for a principal

Own the policy question of which validator your assets publish and what that costs. Revalidation round-trips versus long-lived immutable caching is a bandwidth-and-latency tradeoff you set once for the whole service.

## The signature ```go func ServeContent(w http.ResponseWriter, r *http.Request, name string, modtime time.Time, content io.ReadSeeker) ``` Four of those five arguments are doing real work, and understanding each one is understanding the function. - **`content io.ReadSeeker`** — not an `io.Reader`. `ServeContent` must be able to seek, because that is how it learns the total size (seek to the end, then back) and how it jumps to the start of a requested byte range without reading and discarding everything before it. An `*os.File`, a `*bytes.Reader` and a `*strings.Reader` all qualify; a network stream or a `gzip.Reader` does not. - **`name string`** — used only to derive the content type from the file extension. It does not have to be a real path, and nothing is opened from it. - **`modtime time.Time`** — the modification time used for the `Last-Modified` header and for evaluating `If-Modified-Since` / `If-Unmodified-Since`. - **`w`, `r`** — the response and the request, because the whole job is negotiation between them. ## What it implements: range requests If the request carries `Range: bytes=1000-1999`, `ServeContent` seeks to offset 1000, writes exactly 1000 bytes, and responds `206 Partial Content` with a `Content-Range: bytes 1000-1999/<total>` header. If the range cannot be satisfied at all — say the file is 500 bytes long — it responds `416 Requested Range Not Satisfiable`. For a request naming several ranges it builds a `multipart/byteranges` body. It also advertises the capability up front by sending `Accept-Ranges: bytes` on ordinary 200 responses, which is what tells a client it is allowed to resume. This is what makes a download resumable after a dropped connection and what makes seeking in a video or audio file work at all: the player asks for the byte range around the timestamp the user clicked instead of pulling the whole file. ## What it implements: conditional requests Before writing any body, `ServeContent` evaluates the request's precondition headers: - `If-None-Match` against the `ETag` header — note that `ServeContent` never *computes* an ETag; it only uses one you set on `w.Header()` before calling it. - `If-Modified-Since` against `modtime`. - `If-Match` and `If-Unmodified-Since`, which can produce `412 Precondition Failed`. - `If-Range`, which decides whether a conditional range request gets its partial content or falls back to the full body. When the client's copy is still current, the response is `304 Not Modified` with no body. On a large asset that is the difference between a few hundred bytes on the wire and a few hundred kilobytes. ## The zero-time rule If `modtime` is the zero `time.Time` (or the Unix epoch), `ServeContent` omits `Last-Modified` entirely and does not evaluate the modification-time conditions. This is a deliberate escape hatch for content that genuinely has no meaningful timestamp — but it also means that any file system whose `FileInfo.ModTime()` returns the zero value silently produces validator-free responses. That is not a bug in `ServeContent`; it is the honest consequence of having no timestamp to publish. ## Where it sits relative to `ServeFile` and `FileServer` `http.ServeFile(w, r, name)` opens the named file, stats it, and calls `ServeContent` with the file handle and its `ModTime()`. `http.FileServer` resolves the request path against its root and does the same thing. So all three share one implementation of the range and conditional machinery — `ServeContent` is simply the layer you reach for when the bytes are not a file on disk: a rendered document held in a `bytes.Reader`, a decrypted blob, a generated report. ## What a manual copy loses ```go io.Copy(w, f) // 200 OK, whole body, every time ``` This works, and for a small file over a fast link nobody notices. What you have given up is: resumable downloads, media seeking, `304` revalidation, correct `Content-Length` on some code paths, and the `Accept-Ranges` advertisement. You have also taken on the job of writing the status code and headers yourself, which is more places to get it wrong. The rule of thumb is that if the bytes are seekable, `ServeContent` is strictly better than the copy, and it is one line shorter.

  • Why does http.ServeContent require an io.ReadSeeker rather than an io.Reader?
    Seeking is how it determines the total size — seek to the end and back — and how it jumps directly to the start of a requested byte range. Without that it could neither set `Content-Length` reliably nor serve `206 Partial Content` without reading and discarding the leading bytes.
  • Does http.ServeContent generate an ETag for you?
    No. It evaluates `If-None-Match` against an `ETag` header only if you set one on `w.Header()` before calling it. Left alone, the only validator it can produce is `Last-Modified`, from the `modtime` argument you passed.
  • How do http.ServeFile and http.ServeContent relate?
    `http.ServeFile(w, r, name)` opens and stats the named file, then delegates to `http.ServeContent` with the open handle and the file's `ModTime()`. `ServeContent` is the lower layer you call directly when the bytes are not a file on disk — a generated report in a `bytes.Reader`, for example.

saying these in an interview costs you the question

  • Thinks http.ServeContent computes an ETag from the file contents
  • Passes an io.Reader and expects range support
  • Believes a manual io.Copy still answers Range requests
  • Assumes a zero modtime sends the Unix epoch as Last-Modified
  • Thinks 206 Partial Content signals an error