Skip to content
Control Plane Labs

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/items

What 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