HTTP 415 is Unsupported Media Type: the server refuses the request because its content is in a format this method doesn’t support on this resource (RFC 9110 section 15.5.16). The problem can be the request’s Content-Type, its Content-Encoding, or what the server found when it looked at the data itself.
The usual cause: a missing Content-Type
// Sent as Content-Type: text/plain;charset=UTF-8, which a JSON-only endpoint refuses
await fetch('/api/orders', { method: 'POST', body: JSON.stringify(order) });
// Says what it is
await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(order),
});
fetch() labels a string body text/plain;charset=UTF-8 unless you say otherwise. curl’s -d labels it application/x-www-form-urlencoded; --json (curl 7.82 and later) sends Content-Type: application/json for you.
Tell the client what works
RFC 9110 suggests answering an unsupported media type with an Accept header listing the types that would have worked, and an unsupported content coding with Accept-Encoding. For PATCH, RFC 5789 asks for Accept-Patch:
HTTP/1.1 415 Unsupported Media Type
Accept-Patch: application/merge-patch+json, application/json-patch+json
Content-Type: application/problem+json
{"title": "Unsupported patch format", "status": 415}
415, 406 or 400
Content-Type: application/xml, the server only reads JSON 415 Unsupported Media Type
Accept: application/xml, the server only writes JSON 406 Not Acceptable
Content-Type: application/json, but the body doesn't parse 400 Bad Request
Who sends it
- Spring MVC answers 415 (
HttpMediaTypeNotSupportedException) when no converter can read the request’sContent-Type, or it doesn’t matchconsumes. - ASP.NET Core answers 415 when no input formatter accepts the
Content-Typeof a[FromBody]parameter, or it fails a[Consumes]filter. - Express’s
express.json()doesn’t send 415 for a wrong type: it skips the body, leavingreq.bodyempty (undefinedin Express 5,{}in Express 4). It sends 415 only for an unsupported charset orContent-Encoding. Check the type yourself:
app.post('/api/orders', (req, res, next) => {
if (!req.is('application/json')) return res.status(415).set('Accept', 'application/json').end();
next();
}, express.json(), createOrder);
Gotchas
- Parameters count.
application/json; charset=utf-8is JSON; compare the media type, not the whole header string.req.is()handles that. - Compressed uploads. A server that doesn’t decode compressed request bodies can refuse
Content-Encoding: gzipwith 415; RFC 9110 says to list the codings it does take inAccept-Encoding. - Caching. 415 isn’t heuristically cacheable.
Search 406 415 in the tool below to compare the two.