415 Unsupported Media Type in ASP.NET Core Web APIs: Causes and Fixes

The 415 Unsupported Media Type error is one of those errors that stops you cold. Your endpoint looks correct, your request body looks right, and yet ASP.NET Core refuses to process it. The good news: every root cause has a precise fix, and once you understand how ASP.NET Core content negotiation and input formatting works, this error never catches you off guard again. The complete worked examples โ including edge cases for multipart uploads, custom formatters, and integration test patterns โ are available on Patreon, with production-ready source code you can adapt immediately.
Understanding the 415 Unsupported Media Type error in ASP.NET Core requires knowing how the framework maps incoming request bodies to action parameters. ASP.NET Core doesn't blindly accept any request format โ it matches the Content-Type header against registered input formatters, then uses binding source attributes to decide where to look for the data. When that negotiation fails, you get a 415. This article walks through every scenario where this can go wrong, and the exact fix for each one.
What Does 415 Unsupported Media Type Actually Mean?
HTTP 415 is a client error response code defined in RFC 9110. It tells the client that the server understands the request structure but cannot process it because the media type of the request payload is not supported. In ASP.NET Core terms, this means the framework's input formatting pipeline found no registered formatter capable of deserializing the incoming body with the declared Content-Type.
The key insight: 415 is always about the request's Content-Type โ not the response. If you're seeing this error, the problem is in what the client is sending, not what your API is returning.
How ASP.NET Core Processes Request Bodies
Before diving into causes, it helps to understand the pipeline. When a request arrives at an ASP.NET Core controller action:
- The framework checks the
Content-Typeheader of the incoming request. - It looks for a registered input formatter that can handle that media type.
- It uses the binding source attribute on the action parameter (
[FromBody],[FromForm],[FromQuery], etc.) to determine where to read the data from. - If no formatter matches, or the binding source doesn't align with the request format, ASP.NET Core returns
415.
[ApiController] changes step 3 slightly โ it infers [FromBody] for complex types automatically, which is helpful but can produce surprising behavior when the client sends a non-JSON body.
Cause 1: Wrong or Missing Content-Type Header
This is by far the most common cause. Your client is sending a request without a Content-Type header, or with a type your API doesn't accept โ such as text/plain when your endpoint expects application/json.
ASP.NET Core registers application/json as the default input formatter when you call AddControllers() or AddControllerWithViews(). If the incoming Content-Type doesn't match any registered formatter, the framework returns 415 immediately.
The fix: Ensure every POST, PUT, or PATCH request that sends a body includes Content-Type: application/json. In Postman or similar tools, set the body type to "raw" and the format to "JSON". In programmatic clients, set the Content-Type header explicitly before sending.
This is the most common source of 415 during development when using tools like Postman with default settings โ the default body type is "Text", not "JSON".
Cause 2: Using [FromBody] on a Form-Encoded Request
When a client sends application/x-www-form-urlencoded or multipart/form-data, ASP.NET Core does not route that data through the JSON input formatter. If your action parameter is decorated with [FromBody], the framework will reject the request with 415 because it's looking for a JSON body and the request contains form data.
The fix: Use [FromForm] for form-encoded or multipart payloads. [FromForm] tells the model binder to read from the form collection rather than the body stream. For file uploads, use IFormFile in combination with [FromForm].
The [ApiController] attribute does not infer [FromForm] โ it infers [FromBody] for complex types, so if your client is sending a form but you're not explicitly using [FromForm], you'll hit 415.
Cause 3: The API Doesn't Consume the Required Media Type
If you've decorated your action with [Consumes("application/xml")] or a custom media type, the framework enforces it strictly. Any request with a different Content-Type โ even application/json โ will be rejected with 415.
This is intentional behavior, but it catches developers off guard when they forget that [Consumes] narrows what the endpoint accepts, not broadens it.
The fix: Either remove the [Consumes] constraint to accept the default formatter types, or expand it to include all the types the endpoint should handle: [Consumes("application/json", "application/xml")].
If you need XML support across your entire API, add it globally when registering controllers:
builder.Services.AddControllers()
.AddXmlSerializerFormatters();
This registers application/xml as a supported input and output formatter. Without this, even if you send Content-Type: application/xml, the framework has no formatter to process it and returns 415.
Cause 4: Sending a Body on a GET Request
Sending a request body with a GET verb is technically permitted by HTTP but is widely unsupported in practice. ASP.NET Core's model binding does not read [FromBody] parameters for GET requests by default. If you've accidentally added a complex type parameter to a GET action without a binding source attribute, ASP.NET Core will infer [FromQuery] under [ApiController], not [FromBody] โ and if the client sends a JSON body with Content-Type: application/json, the body is ignored and the response may still return 415 depending on the pipeline configuration.
The fix: Do not send request bodies with GET requests. If you need to pass complex filter parameters to a GET endpoint, model them as query string parameters with [FromQuery]. If the filtering requirements are genuinely complex, consider a POST endpoint explicitly designed for search (a pattern called POST-for-search or "search as a resource").
Cause 5: Content Negotiation Failure with Custom Media Types
If your API needs to accept a non-standard media type โ for example, a webhook payload from a third-party service that sends application/vnd.github.event+json or application/csp-report โ ASP.NET Core will reject it with 415 unless you explicitly register a formatter that handles it.
The fix: Extend the default SystemTextJsonInputFormatter to accept the additional media type:
builder.Services.AddControllers(options =>
{
var jsonFormatter = options.InputFormatters
.OfType<SystemTextJsonInputFormatter>()
.FirstOrDefault();
jsonFormatter?.SupportedMediaTypes.Add("application/vnd.github.event+json");
});
This tells the existing JSON formatter to also accept requests with that content type, without adding a new formatter.
Cause 6: Conflict Between [ApiController] and Explicit Binding
[ApiController] is opinionated. It automatically infers [FromBody] for complex object parameters, which is what you want in most cases. However, if you have an action that accepts a string parameter without any binding attribute, [ApiController] infers [FromQuery] โ not [FromBody]. If the client sends a JSON string body with Content-Type: application/json, the parameter will be empty and the body will fail to bind, sometimes manifesting as a 415.
The fix: Always be explicit about binding sources. Use [FromBody] when you expect a JSON body, [FromQuery] for query string values, and [FromRoute] for route parameters. Rely on [ApiController] inference only for complex types where the intent is obvious.
Cause 7: MaxRequestBodySize Exceeded on File Uploads
On file upload endpoints, exceeding the configured maximum request body size causes ASP.NET Core (specifically the Kestrel or IIS integration layer) to reject the request before it reaches the formatter pipeline. In some configurations this manifests as a 415 rather than the more expected 413 Payload Too Large, because the rejection happens before content type negotiation completes.
The fix: For file upload endpoints that need to accept large payloads, configure the body size limit explicitly. For Kestrel, this is done via KestrelServerOptions. For IIS, the web.config maxAllowedContentLength setting applies. For individual actions, you can use the [RequestSizeLimit] and [DisableRequestSizeLimit] attributes.
For a deeper look at related request lifecycle issues, see the OperationCanceledException in ASP.NET Core: Causes and Fixes article, which covers how request pipeline interruptions propagate.
How to Diagnose a 415 Error Quickly
When you encounter 415 in an environment where you can't immediately inspect the request, use ASP.NET Core's built-in request logging to capture the incoming Content-Type header. With Serilog and UseSerilogRequestLogging(), every request logs its method, path, status code, and duration by default.
If you need more granularity, add a short-circuit middleware early in the pipeline that logs the Content-Type header before routing occurs. This lets you see exactly what the client is sending regardless of whether the request ever reaches your controller.
For local debugging, always check the response body alongside the status code โ ASP.NET Core's Problem Details response for 415 includes the type, title, and status fields defined in RFC 7807, which often makes the cause obvious without further investigation.
For a broader perspective on how content negotiation fits into your API design, the Content Negotiation in ASP.NET Core: JSON vs XML vs MessagePack vs Custom Formatters guide covers the full decision space.
Preventing 415 in Production
A few practices eliminate 415 errors before they reach production:
- Document accepted media types explicitly. Use
[Consumes]and[Produces]attributes alongside OpenAPI documentation so that client teams know exactly what format each endpoint expects. - Write integration tests that send wrong content types. A test that sends
Content-Type: text/plainto a POST endpoint and asserts a415response protects against formatter configuration regressions. - Validate client-generated requests in CI. If your API has mobile or JavaScript clients, run integration tests against the actual client code in your CI pipeline to catch content-type mismatches before they ship.
- Return Problem Details on 415. Enable
AddProblemDetails()in your service registration so that415responses include a structured error body, making client-side debugging faster.
โ Prefer a one-time tip? Buy us a coffee โ every bit helps keep the content coming!
FAQ
Why do I get 415 in production but not locally?
The most common reason is a difference in how your local HTTP client (Postman, Swagger UI) sets headers versus how your production client does. Postman's Swagger import might default to application/json, while your production JavaScript fetch or HttpClient call doesn't set Content-Type explicitly. Always verify the exact headers being sent in production using a request logging middleware or an APM tool.
Does [ApiController] automatically fix Content-Type issues?
No. [ApiController] simplifies model binding inference and enables automatic 400 responses for validation failures, but it does not modify how input formatters are registered or how Content-Type headers are matched. You still need to ensure the registered formatters match what the client sends.
Can I make my ASP.NET Core API accept any Content-Type without error?
Yes, but it's not recommended. You can add a wildcard or custom formatter that accepts any media type, but this removes an important safeguard. A better approach is to explicitly register all the media types your API is designed to handle and document them, rather than silently accepting arbitrary formats.
What is the difference between 415 and 400 Bad Request in ASP.NET Core?
415 means the server understands the request structure but cannot process the body because the media type is not supported โ the format is wrong. 400 means the request is syntactically or semantically invalid โ the format may be correct (JSON), but the content violates a validation rule or is malformed. With [ApiController], model validation failures return 400 automatically via the validation filter, not 415.
How do I support both JSON and XML in the same ASP.NET Core API?
Register XML formatters when you configure AddControllers() using .AddXmlSerializerFormatters() or .AddXmlDataContractSerializerFormatters(). Then either add [Consumes("application/json", "application/xml")] to individual actions or allow the default content negotiation to handle it. Clients that send Content-Type: application/json will use the JSON formatter; clients that send Content-Type: application/xml will use the XML formatter.
Will [Consumes] on a base controller affect all derived controllers?
Yes. [Consumes] applied to a base controller class is inherited by all derived controllers. This is a common source of unexpected 415 errors when a new endpoint is added to a derived controller and the developer doesn't realize the base class is restricting accepted content types.
Why does my webhook handler return 415 for valid payloads?
Third-party webhook providers (GitHub, Stripe, etc.) often send payloads with non-standard Content-Type values like application/vnd.github.event+json or application/x-www-form-urlencoded. Your API's default JSON formatter only accepts application/json. Register the additional media type as described in Cause 5, or read the body directly as a stream and deserialize manually to bypass the formatter pipeline entirely.



