Wird geladen...

ProblemDetails in ASP.NET Core: Saubere Fehlerantworten mit TraceId

Exceptions auf RFC 7807 ProblemDetails abbilden und eine traceId zurückgeben, damit Clients konsistente Fehler sehen und Logs eindeutig zuordenbar sind.

Tritt in einer API eine Exception auf, benötigt der Client trotzdem eine Fehlerantwort, die konsistent, sicher und leicht zu verarbeiten ist. Wenn jedes Endpoint-Pattern ein anderes JSON-Format zurückgibt (oder sogar Stacktraces), wird Client-Code schnell fragil und das Debugging unnötig schwierig.

Ein sauberer Ansatz ist, Fehler im Problem-Details-Format (RFC 7807) zurückzugeben und eine traceId hinzuzufügen, damit sich die fehlerhafte Anfrage eindeutig dem passenden Log-Eintrag zuordnen lässt. In .NET 8 basiert das moderne Setup auf der eingebauten IExceptionHandler-Pipeline zusammen mit AddProblemDetails().


Ziel


Was ist ProblemDetails?

ProblemDetails ist ein standardisiertes Fehler-Payload-Format für HTTP-APIs. Typischerweise enthält es folgende Felder:

RFC 7807 erlaubt außerdem zusätzliche Felder (Extension Members). In diesem Beispiel fügen wir traceId hinzu.


Program.cs Setup (AddProblemDetails + IExceptionHandler)

AddProblemDetails() registriert die Problem-Details-Infrastruktur. AddExceptionHandler<T>() registriert einen Handler, der Exceptions in passende ProblemDetails-Antworten übersetzt.


using System.Diagnostics;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// 1) ProblemDetails + globale Anpassung (fügt traceId zu allen ProblemDetails-Antworten hinzu)
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = ctx =>
    {
        var http = ctx.HttpContext;
        var traceId = Activity.Current?.Id ?? http.TraceIdentifier;

        // traceId zu jeder ProblemDetails-Antwort hinzufügen (ggf. auch bei Validation Errors)
        ctx.ProblemDetails.Extensions["traceId"] = traceId;

        // Sinnvolles Default für instance
        ctx.ProblemDetails.Instance ??= http.Request.Path;
    };
});

// 2) Exception-Handler registrieren (IExceptionHandler)
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();

// 3) Eingebaute Exception-Handling-Middleware
app.UseExceptionHandler();

app.MapControllers();
app.Run();

GlobalExceptionHandler (Exceptions auf Statuscodes abbilden)

IExceptionHandler ermöglicht eine zentrale Exception-to-Response-Logik. Bekannte Exceptions erhalten aussagekräftige HTTP-Statuscodes (404/409/400). Unbekannte Fehler werden als 500 mit einer sicheren (generischen) Nachricht zurückgegeben.


using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

public sealed class GlobalExceptionHandler(
    ILogger<GlobalExceptionHandler> logger,
    IProblemDetailsService problemDetailsService) : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger = logger;
    private readonly IProblemDetailsService _problemDetailsService = problemDetailsService;

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        var (status, title, type, detail) = Map(exception);

        // Vollständige Details serverseitig loggen (nicht im Response ausgeben)
        _logger.LogError(exception, "Unhandled exception ({Status}).", status);

        var problem = new ProblemDetails
        {
            Status = status,
            Title = title,
            Type = type,
            Detail = detail,
            Instance = httpContext.Request.Path
        };

        httpContext.Response.StatusCode = status;

        // Schreibt "application/problem+json" und wendet AddProblemDetails-Customization an (traceId etc.)
        return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = httpContext,
            ProblemDetails = problem,
            Exception = exception
        });
    }

    private static (int status, string title, string type, string detail) Map(Exception ex)
    {
        return ex switch
        {
            NotFoundException nf => (StatusCodes.Status404NotFound, "Not found", "https://httpstatuses.com/404", nf.Message),
            ConflictException cf => (StatusCodes.Status409Conflict, "Conflict", "https://httpstatuses.com/409", cf.Message),
            ValidationException ve => (StatusCodes.Status400BadRequest, "Validation error", "https://httpstatuses.com/400", ve.Message),

            // Unbekannt / unerwartet
            _ => (StatusCodes.Status500InternalServerError, "Unexpected error", "https://httpstatuses.com/500",
                  "Ein unerwarteter Fehler ist aufgetreten.")
        };
    }
}

Beispiel-Exceptions

Kleine, klare Exception-Typen machen das Mapping einfach und gut lesbar:

public sealed class NotFoundException(string message) : Exception(message);
public sealed class ConflictException(string message) : Exception(message);
public sealed class ValidationException(string message) : Exception(message);

Beispiel-Ausgabe

Die Response behält eine vorhersehbare Struktur und enthält traceId:


{
  "type": "https://httpstatuses.com/404",
  "title": "Not found",
  "status": 404,
  "detail": "Customer was not found.",
  "instance": "/api/customers/42",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
ASP.NET Core Clean Error Responses with TraceId

Client-Seite: traceId für Support / Debugging nutzen

Client-Code kann eine freundliche Meldung anzeigen und traceId als Referenz für die Fehleranalyse speichern:


const res = await fetch("/api/customers/42");

if (!res.ok) {
  const contentType = res.headers.get("Content-Type") || "";
  let payload;

  if (contentType.includes("application/problem+json") || contentType.includes("application/json")) {
    payload = await res.json();
  } else {
    // Fallback: HTML/plain-text usw.
    payload = { title: "Unexpected error", detail: await res.text() };
  }

  console.error("API error:", payload);

  // Bei ProblemDetails kann traceId vorhanden sein (sonst leer)
  const traceId = payload?.traceId || payload?.extensions?.traceId;

  alert(`Etwas ist schiefgelaufen.${traceId ? ` Referenz: ${traceId}` : ""}`);
}

Häufige Verbesserungen


TL;DR

  • Für konsistente Fehlerantworten ProblemDetails (RFC 7807) verwenden.
  • In .NET 8 ist der moderne Ansatz: IExceptionHandler + AddProblemDetails().
  • Über CustomizeProblemDetails eine traceId hinzufügen, um Fehler mit Logs zu korrelieren.
  • In Produktion keine Stacktraces zurückgeben.