Start Debugging

Correção: "415 Unsupported Media Type" de um endpoint de minimal API no ASP.NET Core 11

Uma minimal API retorna 415 quando o Content-Type da requisição não corresponde ao que o endpoint faz o binding. Envie Content-Type: application/json para um tipo vinculado ao corpo, ou use [FromForm] para formulários e uploads de arquivos.

Um endpoint de minimal API retorna 415 Unsupported Media Type quando o cabeçalho Content-Type do corpo da requisição não corresponde ao que o route handler está tentando vincular. A causa mais comum: um parâmetro do handler é um tipo complexo vinculado a partir do corpo, o que exige Content-Type: application/json, e o cliente não enviou nenhum content type, enviou text/plain ou enviou dados de formulário. Corrija isso enviando Content-Type: application/json para um corpo JSON, ou anote o parâmetro com [FromForm] quando o cliente envia application/x-www-form-urlencoded ou multipart/form-data. Isso foi verificado no ASP.NET Core 11 sobre .NET 11 com C# 14; o comportamento é idêntico do .NET 8 ao .NET 10.

O erro em contexto

Ao contrário da maioria das exceções, esta nunca chega ao seu código. A camada de binding da minimal API rejeita a requisição antes de o seu handler executar e escreve um 415 cru de volta para o cliente. Não há stack trace, nenhum corpo ProblemDetails por padrão, apenas a linha de status:

HTTP/1.1 415 Unsupported Media Type
Content-Type: application/problem+json
Date: Mon, 06 Jul 2026 09:12:44 GMT

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.16",
  "title": "Unsupported Media Type",
  "status": 415
}

Se você não configurou AddProblemDetails(), obtém um corpo vazio apenas com o status 415. De qualquer forma, a ausência de um stack trace é o indício: esta é uma falha de negociação de conteúdo em nível de framework, não algo lançado dentro do seu handler. A referência de parameter binding da Microsoft Learn documenta isso claramente em sua tabela de falhas de binding: “Wrong content type (not application/json), body, 415.”

Por que isso acontece

Um route handler de minimal API vincula cada parâmetro a partir de uma origem: a rota, a query string, um cabeçalho, um serviço da DI ou o corpo da requisição. Quando um parâmetro é um tipo complexo sem atributo [From*], as minimal APIs inferem que ele vem do corpo da requisição, e o único leitor de corpo configurado por padrão é o leitor do System.Text.Json. Esse leitor é registrado para exatamente um media type: application/json.

Então o framework faz uma verificação de content type antes mesmo de chamar JsonSerializer. Se o Content-Type recebido não for application/json (ou um tipo com sufixo +json compatível), o leitor de corpo recusa a requisição, e as minimal APIs fazem um curto-circuito com 415. Ele não tenta adivinhar. Um Content-Type ausente, text/plain, application/x-www-form-urlencoded ou multipart/form-data falham todos da mesma forma quando o parâmetro de destino espera um corpo JSON.

Esta é uma falha diferente de um 400 Bad Request. Um 400 significa que o content type estava correto, mas o payload JSON estava malformado ou violou a validação. Um 415 significa que o framework nunca sequer tentou ler o corpo porque o content type estava errado. Manter esses dois separados evita que você depure o seu JSON quando o problema real é um cabeçalho. Os três gatilhos usuais:

Reprodução mínima

Aqui está o menor endpoint que produz o erro. CreateProduct é um tipo complexo sem atributo de binding, então as minimal APIs o vinculam a partir do corpo JSON:

// .NET 11, ASP.NET Core 11, C# 14 -- Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();   // so the 415 comes back as problem+json
var app = builder.Build();

app.MapPost("/products", (CreateProduct product) =>
    TypedResults.Created($"/products/{product.Sku}", product));

app.Run();

public record CreateProduct(string Sku, string Name, int Quantity);

Agora envie um corpo sem o cabeçalho de content type. Cada uma destas retorna 415:

# .NET 11 -- no Content-Type header at all
curl -i -X POST http://localhost:5000/products \
  -d '{"sku":"A-100","name":"Widget","quantity":5}'

# .NET 11 -- wrong Content-Type (curl defaults -d to x-www-form-urlencoded)
curl -i -X POST http://localhost:5000/products \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'sku=A-100&name=Widget&quantity=5'

# .NET 11 -- text/plain, even though the payload is valid JSON
curl -i -X POST http://localhost:5000/products \
  -H "Content-Type: text/plain" \
  -d '{"sku":"A-100","name":"Widget","quantity":5}'

O payload na primeira e na terceira chamadas é JSON perfeitamente válido. Isso não importa. O leitor é controlado pelo cabeçalho, não pelos bytes.

Correção, em detalhe

Trabalhe nestes itens em ordem. O primeiro resolve a grande maioria dos casos.

1. Envie Content-Type: application/json para um tipo vinculado ao corpo

Se o seu handler vincula um tipo complexo a partir do corpo, o cliente precisa declarar um content type JSON. Com o curl, a armadilha é que -d (ou --data) define silenciosamente application/x-www-form-urlencoded. Use --json, ou defina o cabeçalho explicitamente:

# .NET 11 -- curl 7.82+ has a --json shortcut that sets the header for you
curl -i -X POST http://localhost:5000/products \
  --json '{"sku":"A-100","name":"Widget","quantity":5}'

# .NET 11 -- or set it by hand
curl -i -X POST http://localhost:5000/products \
  -H "Content-Type: application/json" \
  -d '{"sku":"A-100","name":"Widget","quantity":5}'

A partir de um HttpClient tipado, use PostAsJsonAsync, que define o cabeçalho e serializa em uma única chamada. Esta é a maneira mais comum de acidentalmente corrigir ou acidentalmente quebrar o cabeçalho:

// .NET 11, C# 14 -- sets Content-Type: application/json automatically
using System.Net.Http.Json;

var http = new HttpClient { BaseAddress = new Uri("http://localhost:5000") };
var response = await http.PostAsJsonAsync(
    "/products",
    new { sku = "A-100", name = "Widget", quantity = 5 });

response.EnsureSuccessStatusCode();   // 201 Created, no 415

Se você constrói o HttpContent manualmente, use JsonContent.Create(...) ou um StringContent com o media type definido. Um new StringContent(json) sem media type usa text/plain por padrão e resulta em um 415:

// .NET 11, C# 14
// WRONG -- StringContent defaults to text/plain -> 415
var bad = new StringContent(json);

// RIGHT -- declare the media type
var good = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

No fetch do JavaScript, defina o cabeçalho explicitamente; o fetch não o adiciona para você quando o corpo é uma string:

// browser fetch -- must set Content-Type or you get 415
await fetch("/products", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ sku: "A-100", name: "Widget", quantity: 5 }),
});

2. Use [FromForm] para envios de formulário e uploads de arquivos

Se o cliente genuinamente envia dados de formulário (um submit de <form> HTML, ou um upload de arquivo), não force isso para JSON. Diga ao handler para vincular a partir do formulário em vez do corpo, anotando cada parâmetro com [FromForm]. Isso muda o content type esperado do endpoint para application/x-www-form-urlencoded e multipart/form-data:

// .NET 11, ASP.NET Core 11, C# 14
app.MapPost("/products",
    ([FromForm] string sku, [FromForm] string name, [FromForm] int quantity) =>
        TypedResults.Created($"/products/{sku}", new { sku, name, quantity }));

Para uploads de arquivos, um parâmetro IFormFile exige multipart/form-data. Conforme a documentação de minimal API, as minimal APIs não vinculam o corpo inteiro da requisição diretamente a um IFormFile; o campo precisa vir através de codificação de formulário, e o nome do parâmetro precisa corresponder ao nome do campo do formulário:

// .NET 11, ASP.NET Core 11, C# 14
app.MapPost("/upload",
    async ([FromForm] string title, IFormFile file, HttpContext ctx) =>
    {
        await using var stream = File.Create(Path.Combine("uploads", file.FileName));
        await file.CopyToAsync(stream);
        return TypedResults.Ok(new { title, file.FileName, file.Length });
    })
    .DisableAntiforgery();   // see the gotcha below before you copy this line

Envie isso como multipart e o 415 desaparece:

# .NET 11 -- multipart, matches the [FromForm] + IFormFile handler
curl -i -X POST http://localhost:5000/upload \
  -F "title=Spec sheet" \
  -F "file=@./spec.pdf"

3. Remova o charset ou sufixo de fornecedor que o leitor JSON rejeita

Um content type como application/json; charset=utf-8 é aceito, mas um tipo de fornecedor puro como application/vnd.myapp+json pode não ser, dependendo de como os media types do leitor estão configurados. Se você controla um cliente que envia um media type +json personalizado e não consegue alterá-lo, registre esse media type para que o leitor de corpo JSON o reconheça. Nas minimal APIs você faz isso configurando os content types de requisição aceitos pelo endpoint com Accepts, o que também alimenta o seu documento OpenAPI:

// .NET 11, ASP.NET Core 11, C# 14
app.MapPost("/products", (CreateProduct product) =>
        TypedResults.Created($"/products/{product.Sku}", product))
    .Accepts<CreateProduct>("application/json", "application/vnd.myapp+json");

4. Leia um corpo não-JSON por conta própria com HttpRequest

Quando o payload não é JSON de forma alguma (bytes crus, CSV, um formato de texto personalizado), pare de vincular um tipo complexo e leia o stream diretamente. Vincule HttpRequest (ou Stream, ou PipeReader), que as minimal APIs fornecem sem nenhuma verificação de content type, e faça o parse do corpo nos seus próprios termos:

// .NET 11, ASP.NET Core 11, C# 14 -- accepts any content type
app.MapPost("/import", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    var raw = await reader.ReadToEndAsync();
    // parse `raw` (CSV, custom format, whatever) here
    return TypedResults.Ok(new { bytes = raw.Length });
});

Como você nunca pediu ao framework para desserializar o corpo em um parâmetro tipado, não há controle de content type, e o 415 não pode ocorrer neste endpoint.

Pegadinhas e variantes

Um punhado de casos parecidos leva pessoas a esta página por engano, e algumas arestas afiadas mordem mesmo depois da correção:

O modelo mental a manter: um 415 de minimal API é uma incompatibilidade entre o Content-Type que o cliente enviou e o leitor de corpo que o endpoint espera. Decida o que o endpoint deve aceitar, corpo JSON, formulário, arquivo ou stream cru, e então faça o cabeçalho do cliente e o binding do handler concordarem. Quando eles concordam, o 415 desaparece e você volta ao território normal de 400/200.

Relacionados

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar