Understanding HTTP 415 Unsupported Media Type: Content-Type, Not Content Correctness
415 Unsupported Media Type is a code about format, not content — and that distinction gets lost more often than it should. This post covers what 415 actually promises, how it differs from 400 (malformed content) and 406 (the client rejecting the server's chosen format, rather than the reverse), and a detail that trips people up in practice: express.json() doesn't automatically return 415 for a mismatched Content-Type the way many developers assume.
What Is a 415?
A 415 tells the client that the server understood what it was asking for, but can't process the format the request body was sent in.
The 415 (Unsupported Media Type) status code indicates that the origin server is refusing to service the request because the content is in a format not supported by this method on the target resource. The format problem might be due to the request's indicated Content-Type or Content-Encoding, or as a result of inspecting the data directly. — RFC 9110, Section 15.5.16
In plain English: "I don't understand the format of the data you sent me." Sending XML to an endpoint that only accepts JSON, uploading a .bmp to an endpoint that only handles JPEG and PNG, or sending a Content-Encoding the server can't decompress, are all textbook 415s. Note the RFC's own phrasing: the problem can come from the declared Content-Type/Content-Encoding header, or from the server actually inspecting the bytes and finding they don't match what was claimed — either is a valid basis for a 415.
Don't confuse 415 with 400. Use 415 specifically when the content format or encoding is wrong. Use 400 when the content is in the right format but has validation errors. A malformed JSON payload can go either way — many APIs use 400 if the body can't even be parsed as JSON, reserving 415 for when the
Content-Typeitself declares a format the endpoint doesn't accept at all.
Format vs Content: Where the Line Actually Sits
The recurring confusion with 415 is treating it as a general-purpose "something's wrong with your request body" code. It isn't — it's specifically about the shape of the data, not what's inside it:
- 415 asks: can I even parse or interpret data in this format, on this endpoint? A
Content-Type: application/xmlon a JSON-only endpoint fails here regardless of whether the XML itself is well-formed. - 400 asks: is the request even parseable, given the format it claims to be in?
Content-Type: application/jsonwith a body that isn't valid JSON — a trailing comma, an unclosed brace — is a 400: the format is right, the bytes don't honor it. - 422 asks: once parsed, does the data satisfy the endpoint's business rules? Valid JSON with a
startDateafter anendDateis a 422, not a 415 or 400 — the format and syntax are both fine.
A practical test: if changing the request's Content-Type header to something the server does support would make the request at least parseable (even if it later fails validation), the original response should have been 415. If no Content-Type would have helped because the bytes themselves are broken, that's 400.
The Other Direction: 415 vs 406
415 and 406 Not Acceptable are mirror images of each other, and mixing them up is a common mistake:
- 415 is about the request: the client sent a body in a format the server can't consume.
- 406 is about the response: the client's
Acceptheader specified formats the server can't produce, so the server can't satisfy the client's request for output format.
Put another way: 415 fires on the way in, 406 fires on the way out. An API that only accepts and returns JSON would use 415 if a client POSTs XML, and 406 if a client sends Accept: application/xml on a GET and the server has nothing but JSON to offer.
415 vs 400 vs 406
| Code | Meaning | Use when |
|---|---|---|
| 400 Bad Request | Malformed request | The declared format is fine, but the body doesn't actually parse as that format |
| 415 Unsupported Media Type | Unsupported request format | The Content-Type (or the inspected data) declares a format this endpoint doesn't accept at all |
| 406 Not Acceptable | Unsatisfiable response format | The client's Accept header requests a response format the server can't produce |
Common Causes
The recurring shapes of a genuine 415:
- Sending XML when JSON is expected — or any format mismatch where the endpoint has a fixed, narrow set of formats it understands.
- Wrong
Content-Typeheader — the header doesn't match what the client actually sent, or doesn't match any format the endpoint supports (text/plainagainst a strictlyapplication/jsonAPI). - Unsupported file format — an upload endpoint that only accepts specific image or document types receiving something outside that set.
Returning 415 Correctly
Express / Node.js
This is the detail worth calling out explicitly: express.json() does not return 415 when the Content-Type doesn't match application/json. By default, if the incoming Content-Type doesn't match the parser's configured type option, body-parser (which express.json() wraps) just skips parsing entirely and calls next() — req.body ends up undefined rather than the middleware throwing an error. The 415s that body-parser does throw on its own are narrower: unsupported Content-Encoding values (like an unrecognized compression scheme), not a mismatched Content-Type. If you want a strict 415 for the wrong Content-Type, check it yourself:
import express from "express";
const app = express();
app.use(express.json());
app.post("/api/orders", (req, res, next) => {
// req.is() returns the matching type, false, or null (no Content-Type header)
if (!req.is("application/json")) {
return res.status(415).json({
error: "unsupported_media_type",
message: "This endpoint only accepts application/json.",
accepted: ["application/json"],
});
}
next();
}, (req, res) => {
res.status(201).json(req.body);
});Next.js App Router
Route Handlers get the same freedom (and the same responsibility) — nothing rejects an unexpected Content-Type unless you check for it:
// app/api/orders/route.ts
export async function POST(request: Request) {
const contentType = request.headers.get("content-type") ?? "";
if (!contentType.includes("application/json")) {
return Response.json(
{
error: "unsupported_media_type",
message: "This endpoint only accepts application/json.",
accepted: ["application/json"],
},
{ status: 415 },
);
}
const body = await request.json();
return Response.json(body, { status: 201 });
}NGINX
NGINX doesn't validate Content-Type against application semantics — that's inherently an application-layer decision, since only your code knows which formats a given endpoint supports. Where NGINX does participate is enforcing what it will accept for uploads at the proxy layer, if you use a module like ngx_http_dav_module with restrictions, or simply passing the request (and any 415 the backend returns) straight through:
location /api/ {
proxy_pass http://backend;
# Content-Type validation happens in the application;
# NGINX forwards the request and the resulting 415 unchanged.
}REST API JSON body
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json
Accept: application/json
{"error": "unsupported_media_type", "message": "This endpoint only accepts application/json.", "accepted": ["application/json"]}Including an Accept header in the response (or listing accepted types in the body) gives the client something actionable, mirroring how a well-formed 406 response should list what the server can produce.
Common Pitfalls
- Assuming
express.json()rejects the wrongContent-Typeautomatically. By default it silently skips parsing instead of erroring — if you need a hard 415 for a badContent-Type, checkreq.is()(or the header directly) yourself. - Using 415 for a body that's the right format but broken. Invalid JSON syntax under a correct
Content-Type: application/jsonis a 400 — the format declaration was right, the bytes weren't. - Confusing request-format problems (415) with response-format problems (406). They sit on opposite sides of the exchange — double check which direction the mismatch is actually in before picking a code.
- Returning 415 without saying what formats are accepted. A bare "unsupported media type" leaves the client guessing — list the accepted
Content-Typevalues in the response body. - Trusting the declared
Content-Typewithout ever inspecting the body. The RFC explicitly allows basing a 415 on inspecting the actual data, not just the header — useful for upload endpoints where a client might mislabel a file's real format.
Wrapping Up
415 is about the shape of what arrived, not whether its contents make sense. The rules of thumb:
- 415 means the request body's format isn't supported by this endpoint — a
Content-Type/Content-Encodingproblem, not a data-validation one - It's distinct from 400 (right format, broken bytes) and 406 (the response format the client wants isn't available, the mirror-image problem)
express.json()silently skips parsing on a mismatchedContent-Typeby default — it doesn't throw 415 for you; checkreq.is()if you want that behavior- A useful 415 response tells the client which formats the endpoint actually accepts
For more, see our page on 415 Unsupported Media Type, and 400 Bad Request and 406 Not Acceptable for the codes it's most often confused with.