How does Go's net/http choose a Content-Type when the handler sets none?
answer
- the server guesses when you stay silent
- only the start of the body is examined
- 512 bytes, once, at the first write
- the sniff table has no JSON entry
basics
~20 sOn 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.
solid answer
~50 sIf a handler writes a body without setting Content-Type, the server calls `http.DetectContentType` on at most the first 512 bytes written and uses the result. `DetectContentType` implements the standard MIME sniffing algorithm: it matches known magic bytes — the PNG and GIF signatures, `%PDF-`, and so on — falls back to `text/plain; charset=utf-8` for anything that looks like text, and returns `application/octet-stream` when it can tell nothing. It never returns an empty string. Two consequences bite in practice: JSON has no signature in that table, so a JSON API that forgets the header advertises plain text; and because the guess happens at the first write, setting Content-Type afterwards changes nothing. You can also suppress the automatic header entirely by assigning `nil` to the Content-Type key in `w.Header()`. Setting the type explicitly is always the right move.
code
go · 6 linesfunc icon(w http.ResponseWriter, r *http.Request) {
png := loadIcon() // []byte beginning with the PNG signature
// No Content-Type set: the server sniffs the first bytes and sends image/png.
w.Write(png)
}go deeper
Know that Go fills in a Content-Type for you if you do not, and that the safe habit is to set it yourself as the first line of the handler.
Explain the mechanism: at most 512 bytes, at the first write, through http.DetectContentType, with application/octet-stream as the fallback and text/plain for anything that merely looks like text.
Be ready to diagnose a client that rejects your response over its type: name the sniffing default, explain why a late header change cannot fix it, and reach for nosniff on anything carrying user-supplied bytes.
Decide the policy once: types come from your own metadata, never from guessing, and error paths always carry nosniff. Content sniffing is a small correctness leak that gets expensive when a second consumer starts trusting the header.
### The rule `http.ResponseWriter.Write` is documented to do three things when the handler has not done them: call `WriteHeader(http.StatusOK)`, add a Content-Type derived from the data being written, and — for a small enough response with no forced flush — add Content-Length. The Content-Type it adds is the result of `http.DetectContentType(firstBytes)`. ### What DetectContentType actually does ```go func DetectContentType(data []byte) string ``` It looks at **at most the first 512 bytes** of `data` and implements the WHATWG MIME sniffing algorithm — the same table browsers use. Roughly, in order: 1. **Magic-byte signatures.** `\x89PNG\r\n\x1a\n` gives `image/png`; `GIF87a`/`GIF89a` gives `image/gif`; `%PDF-` gives `application/pdf`; there are entries for JPEG, WebP, several audio and video containers, and a few archive formats. 2. **HTML sniffing.** A leading `<!DOCTYPE html`, `<html`, `<body`, and similar tags give `text/html; charset=utf-8`. 3. **A text test.** If the bytes contain no binary control characters, the answer is `text/plain; charset=utf-8` (with a BOM producing the corresponding UTF-16 charset). 4. **The fallback.** Anything else is `application/octet-stream`. The function is documented always to return a valid MIME type, so there is no "unknown" answer and no empty result. ### Why your JSON comes back as text/plain There is no JSON signature in the sniffing table — JSON is plain UTF-8 text with no magic bytes, and the algorithm deliberately does not try to parse. So a handler that does ```go json.NewEncoder(w).Encode(v) // no Content-Type set ``` produces a response labelled `text/plain; charset=utf-8`. Many clients do not care, because they parse what they asked for. Some do: a strict client that checks the type, a browser deciding whether to render or download, and anything that keys behaviour off the header will treat the response as text. This is the single most common way the sniffing rule shows up in a Go review. ### Why setting the type "later" never helps The guess and the freeze happen together. When the first `Write` runs, the server writes the status line and the whole header map; the sniff has to happen before that, because Content-Type is part of that map. So the correct fix is never "set it after Encode" — it is to set it before anything is written: ```go w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(body) ``` ### Turning the automatic header off Sometimes you want no Content-Type at all — a 204, a response whose type a proxy will supply, a body you deliberately leave untyped. Assigning `nil` to the key suppresses the header the server would otherwise generate: ```go w.Header()["Content-Type"] = nil ``` This is the documented mechanism for suppressing automatically added response headers, and it is different from setting the value to the empty string. ### The security angle, briefly Browsers do their own sniffing on the client side, which is why responses that carry attacker-influenced bytes are usually served with `X-Content-Type-Options: nosniff`. Go's own `http.Error` sets that header on every error response it writes, precisely so a message that happens to start with `<html` is not rendered as a page. If you write your own error helper, copy that behaviour. ### Practical guidance - **Always set Content-Type explicitly** in handlers you own. It costs one line and removes the guess. - **Do not rely on the sniff for correctness**, even when it happens to produce the right answer today — the first bytes of a response can change with the payload. - **Remember the 512-byte window.** A response whose distinguishing bytes appear after the first 512 will be classified by whatever came before them. - **When you serve bytes from disk or from an upload**, decide the type from your own metadata rather than from the content, unless sniffing is exactly what you want.
- What does http.DetectContentType return when it recognises nothing?`application/octet-stream`. The function is documented always to return a valid MIME type, so there is no empty or unknown result — anything that is neither a known signature nor plain text falls through to the octet-stream default.
- How do you stop net/http from adding a Content-Type at all?Assign `nil` to the key: `w.Header()["Content-Type"] = nil`. That is the documented way to suppress a header the server would otherwise generate for you, and it differs from setting an empty string value, which would send an empty header.
- Why does http.Error set X-Content-Type-Options: nosniff?Because the error message is arbitrary text that may have come from user input. `http.Error` labels the body `text/plain; charset=utf-8` and adds `nosniff` so a browser will not re-sniff those bytes and decide to render them as HTML.
saying these in an interview costs you the question
- Thinks no Content-Type is sent when the handler sets none
- Expects a JSON body to be sniffed as application/json
- Believes the whole body is scanned to guess the type
- Sets Content-Type after the first write and expects it to apply
- Assumes DetectContentType can return an empty string