Miroslav Holec
Premium

RESTful chybové struktury v .NET 6

Miroslav Holec   6. května 2022

Vracet chybové struktury v .NET 6 je jednoduché. Háček je jen v tom, že různé verze frameworku se historicky chovaly odlišně a v současné době existuje více možností, jak chybu z API vrátit. V článku vysvětlím pár základních tipů, kterých se stačí držet.

Chybovou strukturu je nutné vrátit pokaždé, když HTTP požadavek neskončí úspěšně. Jedná se tedy o chybové stavy 4xx a 5xx. Všechny chybové stavy by měly vracet vždy stejnou strukturu, aby ji klient mohl snadno zparsovat. Podobu chybových struktur definuje RFC 7807.

Více podrobností popisuji v mém průvodci REST API.

Chybové struktury v .NETu

Vývojáři implementovali zmíněné RFC a vytvořily v .NETu hned 3 chybové struktury, které lze použít:

ProblemDetails
ValidationProblemDetails : ProblemDetails
HttpValidationProblemDetails : ProblemDetails

Zmíněný ProblemDetails implementuje RFC 7807, tedy základní potřebné vlastnosti chybové struktury. Dle tohoto RFC je strukturu možné dále rozšiřovat o libovolné další properties nebo objekty. Přesně to dělají poděděné ValidationProblemDetails a HttpValidationProblemDetails.

ValidationProblemDetails a HttpValidationProblemDetails navíc obsahují objekt errors a jsou tedy vhodné pro validační chyby. Pro validační chyby se používají stavové kody 400 a 422. Osobně bych se doporučil držet pouze stavového kódu 400, protože 422 byl navržen pro jiný specifický účel a ne každý systém (natož vývojář) mu rozumí.

ValidationProblemDetails a HttpValidationProblemDetails se liší pouze v tom, že ValidationProblemDetails podporují ModelState, ze kterého vzniká objekt errors. Vznik HttpValidationProblemDetails (od .NET 6) považuji za zbytečný, nehledě na to, že nemají ani podporu ve statické třídě Results, která se používá pro .NET 6 Minimal APIs.

Generování chyb

Vrátit (Validation)ProblemDetails lze z .NET 6 dvěma způsoby.

  1. explicitně naplníme Response Body instancí jedné z výše zmíněných tříd
  2. provoláme nějakou framework metodu, která vytvoří instanci zmíněných tříd implicitně

Prakticky na všech mých přednáškách doporučuji naprogramovat si jedno místo, které chybové struktury generuje (tedy varianta 1). Já to řeším tak, že nezpracovatelný požadavek ukončím vyhozením vybrané výjimky. Následně mám jeden ExceptionMiddleware, který různé chyby vyhodnocuje a generuje vhodnou chybovou strukturu. Používám ProblemDetails v kombinaci s ValidationProblemDetails a nebo pouze ProblemDetails. Ty lze totiž snadno rozšířit o libovolný errors objekt. Funguje to takto:

var p = new ProblemDetails();
p.Title = "Validation Failed";
p.Status = 400;
p.Extensions.Add("Errors", new
{
    Property1 = "error ab",
    Property2 = "error xy"
});

A výsledek je:

{
  "title": "Validation Failed",
  "status": 400,
  "errors": {
    "property1": "error ab",
    "property2": "error xy"
  }
}

Druhá možnost je vracet chyby z různých míst v kódu s využitím frameworkových metod. Obvykle se tedy jedná o MVC:

public IActionResult Demo()
{
    return Problem();
}

nebo o Minimal APIs:

app.MapGet("/", () =>
{
    return Results.Problem();
});

Zde je ale větší riziko, že API bude vracet polovičatou odpověď. Zejména je nutné dát si pozor na použití metod typu BadRequest() nebo BadRequest(ModelState). První vygeneruje pěknou strukturu:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "status": 400,
  "traceId": "00-bd0b009f94fd15f705dd7f9864fdabe3-602fe24ae23b5a61-00"
}

zatímco druhá možnost s ModelState (ve kterém je jedna chyba) vrátí:

{
  "": [
    "An error occured!"
  ]
}

Shrnutí

  • chyby vracíme vždy, když dojde ke stavu 4xx, 5xx
  • dodržujeme jednotnou chybovou strukturu dle RFC 7807
  • v případě validačních chyb používáme status code 400 (Bad Request)
  • nepoužíváme novou třídu HttpValidationProblemDetails
  • lepší je generovat chyby na jednom místě (middleware)
  • při použití frameworkových metod je nutné hlídat vrácené struktury