415 Unsupported Media Type
Client error response defined by RFC 9110 §15.5.16.
Last updated July 27, 2026
Common causes at a glance
- A JSON body sent without Content-Type, so curl defaults to form encoding
- A charset parameter the server does not accept, such as application/json;charset=UTF-8
- A vendor media type like application/vnd.api+json required but not sent
- A gzipped or brotli-compressed request body the server has no decoder for
Reproduce it
curl -i -X POST -d '{"name":"widget"}' https://api.example.com/v1/itemsWhat 415 Unsupported Media Type means
Content negotiation on the request side is blunt: the server reads
Content-Type, decides whether it has a parser registered for that media
type on that route, and rejects the request if it does not. It does not sniff the body
first. Perfectly valid JSON labelled application/x-www-form-urlencoded gets
a 415 even though the bytes are exactly what the endpoint wants.
The other half of the code covers Content-Encoding. Gzip a request body
against a server with no decoder wired up and that is also 415, a much less obvious
failure because the header you have to look at is not the one anybody checks first.
What the spec says
RFC 9110 §15.5.16 covers both causes explicitly: the
problem may be the indicated Content-Type, the
Content-Encoding, or the result of inspecting the data
directly. It also states what a good server does next: for an unsupported media type,
send Accept listing what it would have taken; for
an unsupported coding, Accept-Encoding. Most APIs
skip this, which is why 415 bodies are so often unhelpful. Note the boundary with
422: 415 means the server cannot parse this format at all, while
422 means it parsed the content fine and rejected what was inside.
What actually causes it
The most common single cause is curl -d, which defaults to
Content-Type: application/x-www-form-urlencoded. Paste a JSON string after
it and the server sees form data containing a stray brace. Close behind is the charset
parameter: strict parsers that match the media type exactly will reject
application/json; charset=utf-8 even though the suffix is harmless and,
for JSON, redundant. JSON:API and similar profiles require a vendor type such as
application/vnd.api+json and reject plain application/json on
purpose. Compression is the quiet one: some clients add
Content-Encoding: gzip to request bodies, which is legal and which most
server stacks will not decode.
How to debug it
Print the request headers your client actually sent, because the assumed value and the real value diverge more often than you would like:
curl -sv -X POST -d '{"name":"widget"}' https://api.example.com/v1/items 2>&1 | grep -i '^> content-'
If that shows content-type: application/x-www-form-urlencoded, the
diagnosis is done: add -H 'Content-Type: application/json'. When the header
already looks right, vary it one parameter at a time, with and without
; charset=utf-8, plain versus vendor subtype, until one works. Check the
response for an Accept or Accept-Encoding header, since a
well-behaved server tells you what it wanted. If a middleware chain added compression,
strip it and retry; request-body compression is the cause people find last.
curl-build generates the corrected command once you know the right
type.
Working examples
curl
# -d alone sends application/x-www-form-urlencoded. This is the usual 415.
curl -i -X POST -d '{"name":"widget"}' https://api.example.com/v1/items
# Correct: label the body.
curl -i -X POST \
-H 'Content-Type: application/json' \
-d '{"name":"widget"}' \
https://api.example.com/v1/items
# Inspect what you actually sent.
curl -sv -X POST -d '{"name":"widget"}' \
https://api.example.com/v1/items 2>&1 | grep -i '^> content-'
Python (requests)
import requests
url = "https://api.example.com/v1/items"
# data= with a string sets no JSON content type -> 415
bad = requests.post(url, data='{"name":"widget"}', timeout=10)
print(bad.status_code, bad.headers.get("Accept"))
# json= sets Content-Type: application/json for you.
ok = requests.post(url, json={"name": "widget"}, timeout=10)
print(ok.status_code)
# Vendor media types have to be set explicitly.
vnd = requests.post(
url,
data='{"data":{"type":"items"}}',
headers={"Content-Type": "application/vnd.api+json"},
timeout=10,
)
print(vnd.status_code)
Node (fetch)
const url = "https://api.example.com/v1/items";
// fetch() guesses text/plain;charset=UTF-8 for a bare string body.
const bad = await fetch(url, { method: "POST", body: '{"name":"widget"}' });
console.log(bad.status, bad.headers.get("accept"));
// Be explicit.
const ok = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "widget" }),
});
console.log(ok.status);Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request