HTTP status codes / 415
415 Unsupported Media Type — HTTP Status Code
415 Unsupported Media Type means the server refused the request because the body is in a format this endpoint does not accept. The usual cause is a missing or wrong Content-Type header, such as sending JSON labeled as form data. Less often the problem is an unsupported Content-Encoding such as compressed request bodies.
What 415 means
RFC 9110 (Section 15.5.16) defines 415 Unsupported Media Type as the origin server refusing the request because its content is in a format this method on this resource does not support. The RFC lists three ways the server might decide that: from the Content-Type header, from the Content-Encoding header, or by inspecting the data directly.
It also suggests how to help the client. If the problem is the content coding, the server ought to send an Accept-Encoding response header listing what it would accept. If the problem is the media type, it can send an Accept response header with the acceptable types.
When you see it
415 nearly always comes from applications and API frameworks, because they are the layer that cares about body formats. Web servers, CDNs and WAFs rarely produce it.
- Typed API frameworks. Spring, ASP.NET Core and similar frameworks match a controller's declared input type against the request's
Content-Typeand return 415 automatically when they do not match. - REST and GraphQL APIs that only accept
application/json. - Upload endpoints that check the uploaded file's type and reject, for example, an executable when only images are allowed.
- Webhook receivers that expect a specific body format from the sender.
Note that not every framework sends 415. Express's express.json() simply skips bodies whose Content-Type does not match, so the handler sees an empty body and usually fails later with a 400 or 422 instead.
Common causes
- Missing Content-Type. A
fetch()call withbody: JSON.stringify(data)but no header sends the body labeled astext/plain;charset=UTF-8. - The wrong label. Sending JSON with
Content-Type: application/x-www-form-urlencoded, whichcurl -ddoes by default. - Multipart where JSON was expected, or the reverse, often after switching HTTP client libraries.
- Compressed request bodies with
Content-Encoding: gzipsent to a server that does not decompress requests. - Strict parameter matching, where a server accepts
application/jsonbut notapplication/json; charset=utf-8, or does not recognize vendor types such asapplication/vnd.api+json.
How to fix it
On the client, always state what you send:
await fetch("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(order),
});
With curl, use --json (curl 7.82 or later), which sets both Content-Type and Accept to JSON, or set the header yourself:
curl -sS -X POST -H "Content-Type: application/json" \
-d '{"sku":"A-100","qty":2}' https://api.example.com/v1/orders
On the server, tell clients what you expect. A minimal Express check that returns a proper 415:
app.post("/api/orders", (req, res, next) => {
if (!req.is("application/json")) {
res.set("Accept", "application/json");
return res.status(415).json({ error: "Send the body as application/json" });
}
next();
}, express.json(), createOrder);
If your API should accept compressed bodies or extra media types, enable them deliberately in the framework configuration rather than loosening type checks across the board.
How to check it
Send the same body twice, once mislabeled and once labeled correctly, and compare the codes:
curl -sS -o /dev/null -w "%{http_code}\n" -X POST \
-d '{"sku":"A-100"}' https://api.example.com/v1/orders
curl -sS -o /dev/null -w "%{http_code}\n" -X POST \
-H "Content-Type: application/json" -d '{"sku":"A-100"}' https://api.example.com/v1/orders
The first request goes out as application/x-www-form-urlencoded because that is what -d uses by default. A 415 on the first and a 2xx on the second confirms the header is the problem. Add -i to see whether the server returns an Accept header naming what it wants.
What it means for uptime monitoring
PingRobot's checker sends a GET with no body, the user agent PingRobot/1.0 (+https://pingrobot.io/bot) and Accept: */*. It follows redirects and treats the final response as up only if its status is 200 to 399, so a 415 would be reported as down.
Since there is no body to reject, a 415 on a monitor usually means the URL is a write endpoint that applies its content-type check to every request. That is a monitor configuration issue: watch a GET-friendly page or health endpoint instead. 415s that real clients receive are an integration problem, best caught in your API logs and tests rather than by an availability check.
Related codes
- 400 Bad Request for bodies that are labeled correctly but malformed
- 422 Unprocessable Content for valid formats with invalid data
- 413 Content Too Large when the body is too big
- 405 Method Not Allowed when the method itself is rejected
- 201 Created, the usual success response for a POST
Start monitoring free — 10 monitors, no credit card.
Frequently asked questions
What is the difference between 415 and 406?
415 is about the format of what the client sent, judged by Content-Type or Content-Encoding. 406 Not Acceptable is about the format the client asked to receive, judged by the Accept header.
Why does my request work in Postman but fail with 415 in code?
Postman usually sets Content-Type automatically based on the body type you choose. In code you often have to set it yourself, and many HTTP libraries do not add it unless you use their JSON helper.
Does charset in Content-Type matter?
It can. Most servers accept application/json with or without charset=utf-8, but a strict server may only match exact values. If a header looks right and still fails, try it without parameters.