Start Debugging

Fix: "415 Unsupported Media Type" von einem Minimal-API-Endpunkt in ASP.NET Core 11

Eine Minimal API gibt 415 zurück, wenn der Content-Type der Anfrage nicht zu dem passt, was der Endpunkt bindet. Senden Sie Content-Type: application/json für einen aus dem Body gebundenen Typ, oder verwenden Sie [FromForm] für Formular- und Datei-Uploads.

Ein Minimal-API-Endpunkt gibt 415 Unsupported Media Type zurück, wenn der Content-Type-Header des Anfrage-Bodys nicht zu dem passt, was der Route-Handler zu binden versucht. Die häufigste Ursache: Ein Handler-Parameter ist ein komplexer Typ, der aus dem Body gebunden wird und daher Content-Type: application/json erfordert, während der Client keinen Content-Type gesendet hat, text/plain gesendet hat oder Formulardaten gesendet hat. Beheben Sie es, indem Sie Content-Type: application/json für einen JSON-Body senden, oder annotieren Sie den Parameter mit [FromForm], wenn der Client application/x-www-form-urlencoded oder multipart/form-data postet. Dies ist gegen ASP.NET Core 11 auf .NET 11 mit C# 14 verifiziert; das Verhalten ist auf .NET 8 bis .NET 10 identisch.

Der Fehler im Kontext

Anders als die meisten Ausnahmen erreicht diese hier niemals Ihren Code. Die Bindungsschicht der Minimal API weist die Anfrage ab, bevor Ihr Handler läuft, und schreibt ein blankes 415 an den Client zurück. Es gibt keinen Stack Trace, standardmäßig keinen ProblemDetails-Body, nur die Statuszeile:

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
}

Wenn Sie AddProblemDetails() nicht eingerichtet haben, erhalten Sie einen leeren Body mit lediglich dem 415-Status. So oder so ist das Fehlen eines Stack Trace der verräterische Hinweis: Dies ist ein Content-Negotiation-Fehler auf Framework-Ebene, nichts, das innerhalb Ihres Handlers geworfen wurde. Die Parameter-Binding-Referenz von Microsoft Learn dokumentiert das in ihrer Tabelle der Bindungsfehler unmissverständlich: “Wrong content type (not application/json), body, 415.”

Warum das passiert

Ein Minimal-API-Route-Handler bindet jeden Parameter aus einer Quelle: der Route, dem Query-String, einem Header, einem Dienst aus DI oder dem Anfrage-Body. Wenn ein Parameter ein komplexer Typ ohne [From*]-Attribut ist, folgern Minimal APIs, dass er aus dem Anfrage-Body stammt, und der einzige standardmäßig eingebundene Body-Reader ist der System.Text.Json-Reader. Dieser Reader ist für genau einen Media-Type registriert: application/json.

Das Framework führt also eine Content-Type-Prüfung durch, bevor es überhaupt JsonSerializer aufruft. Wenn der eingehende Content-Type nicht application/json ist (oder ein kompatibler +json-Suffix-Typ), lehnt der Body-Reader die Anfrage ab, und Minimal APIs brechen mit 415 kurz ab. Es wird nicht versucht zu raten. Ein fehlender Content-Type, text/plain, application/x-www-form-urlencoded oder multipart/form-data schlagen alle auf dieselbe Weise fehl, wenn der Zielparameter einen JSON-Body erwartet.

Dies ist ein anderer Fehler als ein 400 Bad Request. Ein 400 bedeutet, dass der Content-Type korrekt war, aber die JSON-Nutzlast fehlerhaft war oder gegen die Validierung verstieß. Ein 415 bedeutet, dass das Framework nie auch nur versucht hat, den Body zu lesen, weil der Content-Type falsch war. Diese beiden auseinanderzuhalten erspart Ihnen das Debuggen Ihres JSON, wenn das eigentliche Problem ein Header ist. Die drei üblichen Auslöser:

Minimales Repro

Hier ist der kleinste Endpunkt, der den Fehler erzeugt. CreateProduct ist ein komplexer Typ ohne Bindungsattribut, daher binden Minimal APIs ihn aus dem JSON-Body:

// .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);

Posten Sie nun einen Body ohne den Content-Type-Header. Jeder Einzelne davon gibt 415 zurück:

# .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}'

Die Nutzlast im ersten und dritten Aufruf ist vollkommen gültiges JSON. Das spielt keine Rolle. Der Reader ist an den Header gebunden, nicht an die Bytes.

Die Behebung im Detail

Arbeiten Sie diese der Reihe nach durch. Die erste löst die große Mehrheit der Fälle.

1. Senden Sie Content-Type: application/json für einen aus dem Body gebundenen Typ

Wenn Ihr Handler einen komplexen Typ aus dem Body bindet, muss der Client einen JSON-Content-Type deklarieren. Bei curl besteht die Falle darin, dass -d (oder --data) stillschweigend application/x-www-form-urlencoded setzt. Verwenden Sie --json, oder setzen Sie den Header explizit:

# .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}'

Von einem typisierten HttpClient aus verwenden Sie PostAsJsonAsync, was den Header setzt und in einem Aufruf serialisiert. Dies ist die mit Abstand häufigste Art, den Header versehentlich zu korrigieren oder versehentlich zu zerstören:

// .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

Wenn Sie den HttpContent von Hand bauen, verwenden Sie JsonContent.Create(...) oder ein StringContent mit gesetztem Media-Type. Ein new StringContent(json) ohne Media-Type fällt auf text/plain zurück und gibt Ihnen ein 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");

In JavaScript fetch setzen Sie den Header explizit; fetch fügt ihn nicht für Sie hinzu, wenn der Body ein String ist:

// 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. Verwenden Sie [FromForm] für Formular-Posts und Datei-Uploads

Wenn der Client tatsächlich Formulardaten sendet (ein HTML-<form>-Submit oder ein Datei-Upload), zwingen Sie es nicht in JSON. Weisen Sie den Handler an, aus dem Formular statt aus dem Body zu binden, indem Sie jeden Parameter mit [FromForm] annotieren. Das schaltet den erwarteten Content-Type des Endpunkts auf application/x-www-form-urlencoded und multipart/form-data um:

// .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 }));

Für Datei-Uploads erfordert ein IFormFile-Parameter multipart/form-data. Laut der Minimal-API-Dokumentation binden Minimal APIs den gesamten Anfrage-Body nicht direkt an ein IFormFile; das Feld muss über Formular-Encoding kommen, und der Parametername muss mit dem Formularfeldnamen übereinstimmen:

// .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

Posten Sie es als multipart, und das 415 ist weg:

# .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. Entfernen Sie den Charset- oder Vendor-Suffix, den der JSON-Reader ablehnt

Ein Content-Type wie application/json; charset=utf-8 wird akzeptiert, aber ein blanker Vendor-Typ wie application/vnd.myapp+json möglicherweise nicht, je nachdem, wie die Media-Types des Readers konfiguriert sind. Wenn Sie einen Client steuern, der einen benutzerdefinierten +json-Media-Type sendet, und Sie ihn nicht ändern können, registrieren Sie diesen Media-Type, damit der JSON-Body-Reader ihn erkennt. In Minimal APIs tun Sie dies, indem Sie die akzeptierten Anfrage-Content-Types des Endpunkts mit Accepts konfigurieren, was auch Ihr OpenAPI-Dokument speist:

// .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. Lesen Sie einen Nicht-JSON-Body selbst mit HttpRequest

Wenn die Nutzlast überhaupt kein JSON ist (rohe Bytes, CSV, ein benutzerdefiniertes Textformat), hören Sie auf, einen komplexen Typ zu binden, und lesen Sie den Stream direkt. Binden Sie HttpRequest (oder Stream oder PipeReader), was Minimal APIs ohne jegliche Content-Type-Prüfung bereitstellen, und parsen Sie den Body nach Ihren eigenen Regeln:

// .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 });
});

Weil Sie das Framework nie gebeten haben, den Body in einen typisierten Parameter zu deserialisieren, gibt es kein Content-Type-Gate, und das 415 kann auf diesem Endpunkt nicht auftreten.

Fallstricke und Varianten

Eine Handvoll ähnlich aussehender Fälle schickt Leute irrtümlich auf diese Seite, und ein paar scharfe Kanten stechen selbst nach der Behebung noch zu:

Das mentale Modell, das Sie behalten sollten: Ein Minimal-API-415 ist eine Diskrepanz zwischen dem Content-Type, den der Client gesendet hat, und dem Body-Reader, den der Endpunkt erwartet. Entscheiden Sie, was der Endpunkt akzeptieren soll, JSON-Body, Formular, Datei oder rohen Stream, und bringen Sie dann den Header des Clients und die Bindung des Handlers zur Übereinstimmung. Wenn sie übereinstimmen, verschwindet das 415, und Sie sind zurück im normalen 400/200-Terrain.

Verwandt

Quellen

Comments

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

< Zurück