HTTP status code · Client errors (4xx)
415 Unsupported Media Type
415 Unsupported Media Type means the server will not process the request body in the format you sent. Nine times out of ten the body is JSON but the Content-Type header is missing, or says text/plain or form data.
| Class | 4xx, Client errors |
|---|---|
| Defined in | RFC 9110 §15.5.16 |
| Cacheable by default | Only with explicit Cache-Control or Expires |
| Safe to retry | Yes, after fixing the Content-Type header or the body format |
| Relevant headers |
|
What 415 means
RFC 9110, section 15.5.16, says the problem can come from the declared Content-Type, from Content-Encoding, or from the server inspecting the data itself. It also says how to help the client: list acceptable encodings in Accept-Encoding, or acceptable media types in Accept, on the 415 response.
Frameworks differ a lot here. Spring MVC and ASP.NET Core controllers with [FromBody] answer 415 by themselves when the Content-Type does not match a registered reader. Express does not: express.json() simply skips a request that is not application/json, and req.body ends up undefined. FastAPI tends to answer 422 because validation fails on the missing body.
A browser detail causes many of these: fetch() with a string body and no headers sends Content-Type: text/plain;charset=UTF-8. It looks like JSON in the network tab, but the server sees plain text.
Common causes
If you run the server
- fetch() called with body: JSON.stringify(data) but no Content-Type header, so the browser labels it text/plain.
- curl -d sends application/x-www-form-urlencoded by default; a JSON API rejects it unless you add -H "Content-Type: application/json".
- An upload sent as multipart/form-data to an endpoint that expects raw binary or JSON, or the reverse.
- A gzip or br Content-Encoding on the request body that the server does not decompress; body-parser with inflate: false answers 415 in that case.
- A charset the endpoint refuses, such as application/json; charset=latin1 on a strict API.
How to fix it
If you run the server
- Set the header that matches the body: headers: { "Content-Type": "application/json" } in fetch, or let axios set it by passing an object instead of a string.
- With curl, use --json '{"a":1}' (curl 7.82+), which sets both Content-Type and Accept to JSON.
- For file uploads, pass a FormData object and do not set Content-Type yourself; the browser adds the multipart boundary.
- On the server, return an Accept header with the 415 and a message naming the expected type, so client developers do not have to guess.
How to send 415
app.post('/orders', express.json(), (req, res) => {
if (!req.is('application/json')) {
res.set('Accept', 'application/json');
return res.status(415).json({ error: 'Send the body as application/json' });
}
// ...
});// app/orders/route.ts
export async function POST(request: Request) {
const type = request.headers.get('content-type') ?? '';
if (!type.startsWith('application/json')) {
return Response.json(
{ error: 'Send the body as application/json' },
{ status: 415, headers: { Accept: 'application/json' } }
);
}
const order = await request.json();
// ...
}mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {
mediatype, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || mediatype != "application/json" {
w.Header().Set("Accept", "application/json")
http.Error(w, "send the body as application/json", http.StatusUnsupportedMediaType) // 415
return
}
// ...
})from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
@app.post("/orders")
async def create_order(request: Request):
if not request.headers.get("content-type", "").startswith("application/json"):
raise HTTPException(status_code=415, detail="Send the body as application/json",
headers={"Accept": "application/json"})
order = await request.json()
...# Form-encoded by default: a JSON API may answer 415
curl -X POST -d '{"sku":"A1"}' https://api.example.com/orders
# Correct: declare the type (or use --json on curl 7.82+)
curl -X POST -H 'Content-Type: application/json' -d '{"sku":"A1"}' https://api.example.com/ordersCommonly confused with
- 415 vs 422
- 422 means the format was understood but the content is invalid (a required field missing); 415 means the format itself is not accepted.
- 415 vs 406
- 406 is about the response format the client asked for in Accept; 415 is about the request format the client sent.
- 415 vs 400
- 400 fits a body with the right Content-Type that fails to parse, such as broken JSON.
Frequently asked questions
- Why do I get 415 when sending JSON with fetch?
- Because fetch() with a string body defaults to Content-Type: text/plain;charset=UTF-8. Add headers: { "Content-Type": "application/json" } to the request.
- How do I fix 415 in Spring Boot?
- The @RequestBody parameter has no converter for the incoming Content-Type. Send Content-Type: application/json, or if the client posts a form, bind with @ModelAttribute or @RequestParam instead of @RequestBody.
- Should I set Content-Type for multipart uploads?
- No, not by hand. When you pass FormData to fetch or axios, the browser sets multipart/form-data with the correct boundary. Setting it yourself drops the boundary and the server cannot parse the parts.
- Is 415 or 400 correct for a wrong Content-Type?
- 415. RFC 9110 defines it exactly for content in a format the resource does not support, whether judged by Content-Type, Content-Encoding or by inspecting the data.
Last reviewed by Arielton Oberek.