skip to content

A client POSTs a JSON body but sends no Content-Type header, or sends `Content-Type: text/plain`. Why do many HTTP servers answer 415 Unsupported Media Type, and what should that response include?

level: juniorimportance: should knowfreq 42%

answer

  1. parser chosen by label, not by sniffing
  2. no Content-Type ⇒ no parser ⇒ 415
  3. curl defaults to form-urlencoded
  4. JSON requirement blunts cross-origin CSRF
  5. answer with Accept-Post + what was received

basics

~20 s

The server routes a body by its declared Content-Type, not by sniffing the bytes. With no Content-Type or the wrong one, there is no parser to hand it to, so it returns 415 — ideally with an Accept-Post header naming the media types it accepts.

solid answer

~50 s

A server picks a body parser from the **declared** `Content-Type`. It does not look inside the bytes. So a JSON payload labelled `text/plain`, or labelled with nothing at all, has no parser assigned to it and the endpoint refuses with **415 Unsupported Media Type**. This is deliberate rather than pedantic. Sniffing content is a security problem — it is how a payload gets treated as a type nobody intended — and CSRF defences lean on it: a `Content-Type: application/json` requirement makes a cross-origin form POST (which can only send `text/plain`, `application/x-www-form-urlencoded` or `multipart/form-data`) unable to reach a JSON endpoint. An endpoint that cheerfully parses JSON regardless of label throws that protection away. A good 415 response carries `Accept-Post: application/json` (or `Accept-Patch` for PATCH) plus a problem-detail body explaining what was received and what is accepted. The client's fix is to set the header correctly and retry.

code

bash · 7 lines
bash
curl -i -X POST https://api.example.com/users -d '{"name":"Ada"}'
# -> 415 Unsupported Media Type

curl -i -X POST https://api.example.com/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Ada"}'
# -> 201 Created

go deeper

for a junior

Say the server chooses its parser from the Content-Type label and refuses when there is no match; know the curl -H fix.

for a middle

Add the other 415 triggers (unsupported request Content-Encoding, per-method restrictions) and the Accept-Post recovery header.

for a senior

Lead with the security rationale: no sniffing, and the CSRF barrier a strict JSON requirement provides; specify the problem-detail response shape.

for a principal

Set the estate-wide rule that endpoints declare consumed types explicitly and never sniff, and make the 415/400/422 mapping uniform so clients can automate their retry logic.

## What the server is actually doing When a request arrives with a body, the server must decide **how to interpret those bytes**. HTTP's answer is the `Content-Type` header: the sender labels its own payload, and the recipient trusts the label to select a parser. A typical framework endpoint declares which types it consumes — JSON only, say. The dispatch logic is: 1. Read `Content-Type` from the request. 2. Find a configured reader for it that this handler accepts. 3. If none matches → **415 Unsupported Media Type**. No step inspects the body. `{"name":"Ada"}` labelled `text/plain` therefore fails, even though a human can see it is JSON. ## Missing Content-Type RFC 9110 says a recipient that receives a payload with no `Content-Type` **may** assume `application/octet-stream` or attempt to sniff — but strict servers do neither for API endpoints. In practice: no header, no parser, 415. This is why `curl -d '{"a":1}' https://api/…` so often fails with 415 — curl defaults to `application/x-www-form-urlencoded` unless you pass `-H 'Content-Type: application/json'`, which is one of the most common first-day API-debugging moments there is. ## Why the strictness is a feature, not pedantry **Security — content sniffing.** Deciding a payload's type from its bytes is a well-known hazard class; the whole point of `X-Content-Type-Options: nosniff` on the response side is to stop it. The same discipline applies on the request side: honour the label or refuse. **Security — CSRF.** A cross-origin HTML form can only produce three media types: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`. It cannot send `application/json` without triggering a CORS preflight, which your server can reject. So an endpoint that **requires** `Content-Type: application/json` is meaningfully harder to attack from an attacker's page. An endpoint that parses JSON out of a `text/plain` body has silently defeated that. This is the strongest argument for returning 415 rather than being helpful. **Correctness.** `charset` and structured-suffix parameters ride on `Content-Type`; ignoring the header means ignoring those too. ## Related 415 triggers - **Wrong subtype:** `application/xml` to a JSON-only endpoint. - **Unsupported request `Content-Encoding`:** the client gzips its POST body and the server cannot or will not decompress it. This is still a 415 — the payload's *format*, broadly, is unsupported. - **Type supported elsewhere but not here:** `multipart/form-data` accepted by the upload endpoint but not by this one; 415 is per method and per resource. - **Parameter mismatch:** an endpoint that requires `application/vnd.example+json;version=2` and receives `version=1` may reasonably 415. ## What a good 415 looks like ``` HTTP/1.1 415 Unsupported Media Type Accept-Post: application/json Content-Type: application/problem+json { "type": "about:blank", "title": "Unsupported Media Type", "detail": "Expected application/json; received text/plain", "status": 415 } ``` The important parts: - **`Accept-Post`** (or `Accept-Patch` on a PATCH) tells the client exactly what to send. Note there is no `Accept-Put` header; for PUT, list the types in the body. - **Say what was received** as well as what is expected — that single line saves enormous debugging time. - **Do not echo the raw body back**; the client already has it and echoing invites reflection issues. ## What 415 must not be used for If the `Content-Type` was correct and the body simply failed to parse or failed validation, 415 is the wrong signal — it tells the client to change its Content-Type, which will not help. Use 400 for malformed syntax and 422 (or 400) for a valid document that fails business rules. ## Client-side fix Set the header. With curl: ``` curl -X POST https://api.example.com/users \ -H 'Content-Type: application/json' \ -d '{"name":"Ada"}' ``` Most SDKs set it automatically when you pass a JSON body; hand-rolled `fetch` calls and shell scripts are where it goes missing.

  • Why not just sniff the body and parse it as JSON when it obviously is JSON?
    Because content sniffing is a security anti-pattern, and because requiring application/json is a real CSRF barrier — a cross-origin HTML form can only send urlencoded, multipart or text/plain bodies, so a strict JSON requirement keeps those requests out without a preflight the server can reject. Sniffing throws that protection away for a small convenience.
  • A client gzips its POST body and the server cannot decompress it. Which status?
    415 Unsupported Media Type. The request payload's encoding is a format the server does not support, which is the same class of failure as an unsupported Content-Type. The response should state which request Content-Encodings are acceptable, in practice usually identity only.

A customs officer routes a crate by its declaration form, not by prying it open. Unlabelled crate, no lane to send it down — it gets turned away.

saying these in an interview costs you the question

  • Claiming the server should detect JSON from the body's first character.
  • Saying a missing Content-Type is harmless because the server can default it.
  • Using 415 for a JSON body that fails validation.
  • Returning 415 with no indication of what the endpoint does accept.
  • Assuming curl sends application/json by default when you pass -d.

context