# 415 Unsupported Media Type

> 415 Unsupported Media Type: the server won't accept your Content-Type. Real Spring, DRF and ASP.NET errors, curl reproduction, fixes for fetch and FormData.

Source: https://howhttpworks.com/status-codes/415
Last reviewed: 2026-10-05

> **TL;DR:** 415 Unsupported Media Type means the server rejects the format of your request body. Match `Content-Type` to the endpoint, serialize JSON with `JSON.stringify`, and let the client generate the multipart boundary for uploads. Check `Content-Encoding` if the body is compressed.

## What you see in your client

The messages and response bodies here are illustrative. Client messages use `https://api.example.com/resource`; reason phrases and URLs can vary.

- **Axios:** `AxiosError: Request failed with status code 415` with the default status handling. Inspect `error.response.data` for the server's explanation. [Axios source](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **fetch:** the promise resolves with `response.status === 415` and `response.ok === false`. Check the status before reading the body; HTTP errors do not enter `catch` automatically. [MDN fetch](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch).
- **Python requests:** `requests.exceptions.HTTPError: 415 Client Error: Unsupported Media Type for url: https://api.example.com/resource` when you call `response.raise_for_status()`. [Requests source](https://requests.readthedocs.io/en/latest/_modules/requests/models/#Response.raise_for_status).
- **curl -f:** `curl: (22) The requested URL returned error: 415`. Use `curl -i` without `-f` while inspecting the error body. [curl source](https://github.com/curl/curl/blob/master/lib/http.c).
- **.NET:** `HttpRequestException` with the English message `Response status code does not indicate success: 415 (Unsupported Media Type).` after `EnsureSuccessStatusCode()`. [Runtime message template](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/Resources/Strings.resx).
- **Spring WebClient:** `WebClientResponseException$UnsupportedMediaType: 415 Unsupported Media Type from POST https://api.example.com/resource` with `retrieve()`. Read `getResponseBodyAsString()`. [Spring 6.2 source](https://github.com/spring-projects/spring-framework/blob/6.2.x/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java).
- **Spring RestTemplate (6.2):** `HttpClientErrorException$UnsupportedMediaType: 415 Unsupported Media Type on POST request for "https://api.example.com/resource": [no body]` with the default error handler and an empty response body. Read the exception body when present. [Message builder](https://github.com/spring-projects/spring-framework/blob/6.2.x/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java), [exception subclasses](https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/web/client/HttpClientErrorException.html).

## What it means

RFC 9110 section 15.5.16: the origin server refuses the request because the content is in a format the target resource does not support for this method. It is a check on the *request* body format. It has nothing to do with the size (see [413](https://howhttpworks.com/status-codes/413)) or the content being invalid inside a supported format (see [422](https://howhttpworks.com/status-codes/422)).

```http
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json
Accept-Post: application/json, application/ld+json

{"error":"Unsupported Media Type","detail":"Content-Type 'text/plain' is not supported"}
```

`Accept-Post` (or `Accept-Patch`) on the response tells you what to send instead.

## Reproduce it

`curl -d` defaults to `application/x-www-form-urlencoded`; a JSON-only endpoint can reject that type:

```bash
# Triggers 415 on a JSON-only endpoint
curl -sv -X POST https://api.example.com/orders -d '{"sku":"A1","qty":2}'
# > Content-Type: application/x-www-form-urlencoded

# Fixed
curl -sv -X POST https://api.example.com/orders \
  -H 'Content-Type: application/json' \
  -d '{"sku":"A1","qty":2}'
```

## Real error strings by framework

Spring builds the Content-Type message in its exception constructor, and DRF defines the unsupported-media message in its exception class. [Spring source](https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java), [DRF source](https://github.com/encode/django-rest-framework/blob/master/rest_framework/exceptions.py).

| Stack | What you see |
| --- | --- |
| Spring Boot | `HttpMediaTypeNotSupportedException: Content-Type 'text/plain;charset=UTF-8' is not supported` (the route's `consumes` or the missing `HttpMessageConverter`) |
| Django REST framework | `{"detail":"Unsupported media type \"text/plain\" in request."}` |
| ASP.NET Core | 415 when `[Consumes]` or the input formatters do not match; the error body depends on API configuration |
| Express | `express.json()` skips unmatched media types, but unsupported encodings or charsets can produce 415 |
| FastAPI | Request body validation errors return 422; inspect the `detail` array ([FastAPI docs](https://fastapi.tiangolo.com/tutorial/handling-errors/#requestvalidationerror-vs-validationerror)) |

## Common causes by stack

- **Spring Boot / Spring MVC:** a route's `consumes` condition or the available message converters reject the body type, raising `HttpMediaTypeNotSupportedException` and returning 415; compare `@PostMapping(consumes=...)` with the actual request header. [Spring exception mappings](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html).
- **Django REST framework:** accessing `request.data` raises `UnsupportedMediaType` when no configured parser accepts the request type; add the intended parser to `parser_classes` or send a supported type. [DRF exceptions](https://www.django-rest-framework.org/api-guide/exceptions/#unsupportedmediatype).
- **ASP.NET Core controllers:** `[Consumes("application/json")]` rejects another request type with 415; compare `[FromBody]` with `[FromForm]` and the configured input formatters. [Microsoft API documentation](https://learn.microsoft.com/en-us/aspnet/core/web-api/#define-supported-request-content-types-with-the-consumes-attribute).
- **Express / body-parser:** `inflate: false` rejects compressed bodies; unsupported encodings and charsets set status 415 with `encoding.unsupported` or `charset.unsupported`. Log `err.type` and send an accepted encoding or charset. [Parser errors](https://expressjs.com/en/resources/middleware/body-parser/#errors).
- **AWS API Gateway REST APIs:** `passthroughBehavior: NEVER` rejects a content type without a matching mapping template; `WHEN_NO_TEMPLATES` also rejects unmatched types when a template exists. Add the matching template or deliberately change passthrough behavior. [AWS integration docs](https://docs.aws.amazon.com/apigateway/latest/developerguide/integration-passthrough-behaviors.html).

## Fix it

### Clients

```javascript
// JSON: set the header and serialize the body
await fetch('/api/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ sku: 'A1', qty: 2 })
})

// Multipart: leave Content-Type unset; the browser adds the boundary
const form = new FormData()
form.append('file', fileInput.files[0])
await fetch('/api/upload', { method: 'POST', body: form })
```

If you set `Content-Type: multipart/form-data` yourself the request goes out without `boundary=...`, and servers reject it or fail to parse it.

Other client-side causes: PATCH with `application/json` when the API wants `application/merge-patch+json` or `application/json-patch+json`; a charset or vendor type (`application/vnd.api+json`) the server does not list; a request body compressed with `Content-Encoding: gzip` to an endpoint that does not decompress it.

### Servers

```java
// Spring: accept the types you intend, or you get 415 for everything else
@PostMapping(path = "/orders", consumes = { "application/json", "application/*+json" })
public Order create(@RequestBody OrderRequest body) { ... }
```

```javascript
// Express: parse each type you accept, and answer 415 yourself for the rest
app.use(express.json())
app.post('/orders', (req, res) => {
  if (!req.is('application/json')) {
    res.set('Accept-Post', 'application/json')
    return res.status(415).json({ error: 'Content-Type must be application/json' })
  }
  res.status(201).json(createOrder(req.body))
})
```

For DRF, check both `DEFAULT_PARSER_CLASSES` in settings and the view's `parser_classes`; the view can override the global list. `JSONParser` handles `application/json`, `FormParser` handles `application/x-www-form-urlencoded`, and `MultiPartParser` handles `multipart/form-data` uploads. A view limited to `[JSONParser]` rejects a browser form when it reads `request.data`. Add form parsers only to endpoints intended to accept those formats. For a raw file upload, `FileUploadParser` matches `*/*` and should generally stand alone; use `MultiPartParser` for browser multipart uploads. The distinction determines whether the body is read as one file or as named form fields. [DRF parser configuration](https://www.django-rest-framework.org/api-guide/parsers/).

```python
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView

class UploadView(APIView):
    parser_classes = [FormParser, MultiPartParser]

    def post(self, request):
        return Response({'fields': list(request.data.keys())})
```

Compare the client's `Content-Type` and `Content-Encoding` with the headers logged at the application. If they differ, inspect the proxy or gateway transformation. API Gateway can itself return 415 before calling the backend when its mapping-template passthrough rules reject the incoming media type. [AWS passthrough behavior](https://docs.aws.amazon.com/apigateway/latest/developerguide/integration-passthrough-behaviors.html).

## Debug the headers and the bytes together

`Accept: application/json` asks for a JSON response; it does not label the request body. A POST can have that header and still send `Content-Type: text/plain`. Inspect both directions with `curl -v`, then set `Content-Type: application/json` on the outgoing body. With Python requests, `requests.post(url, json=data)` serializes JSON and sets its type, while `data=data` with a dictionary form-encodes it. [Requests serialization source](https://requests.readthedocs.io/en/latest/_modules/requests/models/#PreparedRequest.prepare_body).

For multipart uploads, inspect the boundary in the outgoing header and the body delimiters. Browser `FormData` generates both; setting the header yourself prevents the browser from adding its boundary parameter. Let curl do the same work with `curl -v -F 'file=@report.txt' https://api.example.com/upload`. The endpoint must accept multipart data as well as the particular file or part types it validates. [MDN FormData guidance](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects), [curl manual](https://curl.se/docs/manpage.html#-F).

For compressed requests, `Content-Encoding: gzip` describes compression applied to the body; `Content-Type` still describes the decoded format. Removing the encoding header while sending compressed bytes leaves the parser reading gzip as JSON. Send an uncompressed JSON body or enable the server's supported decompression path. RFC 9110 allows a 415 response to advertise accepted request codings with `Accept-Encoding`; inspect that response header before changing compression. [RFC 9110 section 15.5.16](https://www.rfc-editor.org/rfc/rfc9110#name-415-unsupported-media-type).

## 415 versus neighbors

- [400 Bad Request](https://howhttpworks.com/status-codes/400): the request is malformed (for example JSON that does not parse). The type was acceptable, the content was not.
- [422 Unprocessable Content](https://howhttpworks.com/status-codes/422): JSON parsed fine but failed validation.
- [406 Not Acceptable](https://howhttpworks.com/status-codes/406): the problem is your `Accept` header, not your `Content-Type`.
- [413 Content Too Large](https://howhttpworks.com/status-codes/413): the right type, too many bytes.
