pingrobot

HTTP status codes / 415

415 Unsupported Media Type — HTTP Status Code

· 3 min read

Short answer

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-Type and 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

  1. Missing Content-Type. A fetch() call with body: JSON.stringify(data) but no header sends the body labeled as text/plain;charset=UTF-8.
  2. The wrong label. Sending JSON with Content-Type: application/x-www-form-urlencoded, which curl -d does by default.
  3. Multipart where JSON was expected, or the reverse, often after switching HTTP client libraries.
  4. Compressed request bodies with Content-Encoding: gzip sent to a server that does not decompress requests.
  5. Strict parameter matching, where a server accepts application/json but not application/json; charset=utf-8, or does not recognize vendor types such as application/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.

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.