REST: Standardisierte Fehlermeldungen mittels RFC 7807 Problem Details

Keine Kommentare

REST-Fehlermeldungen: Einleitung

Wenn man eine REST-Schnittstelle implementiert, kommt schnell die Frage auf, wie man Fehler am besten zurückgibt.

Die erste und naheliegendste Option sind die HTTP-Statuscodes (4xx, 5xx je nach Problem) – diese sind aber in ihrer Ausdrucksstärke beschränkt. So drückt z. B. ein 422 aus, dass der Client einen Fehler gemacht hat, aber nicht, welcher genau es war.

Ein kleines Codebeispiel:

@routes.get('/get_widget')
    def get_widget(request, widget_id):
       widget_repository = WidgetRepository()
       if not widget_repository.exists(widget_id):
           return 404, "The widget was not found"
       else:
           return widget_repository.get(widget_id)

Es ist daher am besten, neben dem reinen Statuscode noch einen Body mit zurückzugeben. Ab diesem Punkt aber fangen einige Probleme an:
 

  1. Welches Format hat der Body? Ist es reiner Text oder JSON oder XML oder etwas ganz anderes?
  2. Welche Informationen sollen in dem Body enthalten sein? Soll er nur einen Fehlertext enthalten oder auch weitergehende Informationen?
  3. Gibt es für das Projekt/die Firma bereits einen Standard, an den ich mich halten muss?
  4. Soll die Meldung für Maschinen lesbar sein? Was ist mit Menschen?

Hier öffnet sich plötzlich ein großes Spektrum an Möglichkeiten – dabei wollten wir nur einen Fehler zurückgeben. Mit den RFC 7807 Problem Details gibt es aber einen neueren Standard, der hilft, diese Fragen zu beantworten.

Warum nicht die Framework-Standardbibliothek?

REST-Frameworks wie Spring, ASP.NET oder Django bieten oft vorgefertigte Rückgabeklassen an, allerdings sind diese spezifisch für das Framework. Gerade in Microservice-Architekturen mit verschiedenen Programmiersprachen ist das nicht optimal – zum einen sollte nach außen immer derselbe Fehlertyp kommuniziert werden, zum anderen muss der aufrufende Service-Fehler auch auslesen können – d.h. man muss diese Klassen ggf. in völlig anderen Programmiersprachen nachimplementieren. Zudem sind diese Klassen oft nicht gut erweiterbar – dabei stellt sich oft aber die Notwendigkeit, weitere Informationen an einen Fehler anzuhängen, z. B. eine Korrelations-ID oder aber Informationen, was im vorherigen Aufruf schiefging.

RFC 7807 Problem Details

Der Kern

RFC 7807 (siehe Spezifikation) definiert im Kern ein schlankes JSON-Format, um Fehler zurückzugeben. Die minimalste Version sieht so aus:

HTTP/1.1 404 Not found
Content-Type: application/problem+json
Content-Language: en

{
"type": "https://example.net/widget-not-found",
"title": "Your request parameters didn't validate.",
}
    

Wie man sieht, nutzt es einen neuen Mediatype: application/problem+json. Es gibt auch eine XMl-Variante, aber auf die gehe ich hier nicht ein.

Das JSON an sich enthält erst einmal zwei Felder:

Type

ist ein (ggf. relativer) URL oder about:blank und gibt den Typ des Fehlers an. Idealerweise verweist es gleichzeitig auf einen URL, der weitere Informationen zu dem Fehler gibt. Ich würde hier aber den Anspruch erstmal herunterschrauben und z. B. pro Microservice einen Typ vergeben, sodass man die Quelle des Problems erkennen kann. Ob weiterer Aufwand nötig und sinnvoll ist, ergibt sich dann aus der Nutzung.

Title

sollte das Problem grob eingrenzen und beschreiben. Es sollte aber keine detaillierte Fehlerbeschreibung und statisch sein – also z. B. nicht die fehlerhaften Werte auflisten.

Optionale Felder

Zusätzlich zu diesen Feldern gibt es noch weitere Felder, die enthalten sein können, aber nicht müssen.

Status

(Zahl) Dieses Feld enthält den HTTP-Statuscode, der zurückgegeben wurde. Dieses Feld ist überraschend nützlich, weil so z. B. der Statuscode schnell auch in Logs steht, ohne dass man den Response Body und Response Header zugleich auswerten muss.

Details

sollte Details zu dem Fehler enthalten. Hier gibt der RFC vor, dass dieses Feld keine Debugging-Informationen enthalten sollte. Auf der anderen Seite kann es Details dazu enthalten, was genau das Problem ist. Es bietet sich an, hier z. B. Lösungsmöglichkeiten oder zumindest eine Erklärung unterzubringen, z. B. „Das widget X existiert nicht, bitte wählen Sie ein anderes Widget“ oder „Sie wollten 50 Euro abbuchen, haben aber nur noch 20 Euro Guthaben“.

Instance

Die Instanz des Problems als (ggf. relativer) URL. Hier kann man z. B. die Instanz des Services angeben, der das Problem hat oder aber der URL, der aufgerufen wurde.

Hier ein vollständiges Beispiel:

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en

{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
}
    

Erweiterungen

Zusätzlich können dem JSON beliebige weitere Felder hinzugefügt werden, die z. B. applikationsspezifische Informationen enthalten können. Die offizielle Spec nutzt dabei als Beispiel dies:

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en

{
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "balance": 30,
    "accounts": ["/account/12345",
                "/account/67890"]
}

Hier wurde die aktuelle Account-Balance direkt als Feld hinzugefügt. Zudem gibt es direkte Links auf die Accounts, die der Nutzer hat.

Einerseits ist diese Erweiterbarkeit unglaublich nützlich, da man wichtige Informationen direkt in der Nachricht unterbringen kann. Andererseits können sich Probleme ergeben, wenn jeder Fehlercode eigene Felder definiert und der Client dann wissen muss, welche Felder er deserialisieren kann und muss.

Insbesondere für statisch typisierte Sprachen wie Java oder C# bietet es sich daher eher an, ein generisches „error“-Feld hinzuzufügen, das selbst nur Key-Value Pairs enthält. Diese dann, je nach Bedarf, abzufragen fällt deutlich leichter als jeweils unterschiedliche Fehlerobjekte, in Abhängigkeit vom Type, zu deserialisieren.

RFC 7807 Problem Details in der Praxis

RFC-7807-konforme Rückgabetypen zu schreiben ist einfach – etwas schwieriger kann es aber sein, den gesamten Web-Stack dazu zu bringen, RFC 7807 zu sprechen. Das sollte er aber, denn je nach Route/Fehlercode/Mondphase unterschiedliche Typen als Antwort zu bekommen ist sehr unschön. Daher bietet sich an, mit den Framework-Möglichkeiten zu arbeiten.

Java Spring Boot

Für Spring Boot gibt es ein Package von Zalando, das RFC 7807 einfach zu nutzen macht:
problem-spring-web

@RestController
@RequestMapping("/products")
class ProductsResource {

    @RequestMapping(method = GET, value = "/{productId}", produces = APPLICATION_JSON_VALUE)
    public Product getProduct(String productId) {
        // TODO implement
        return null;
    }

    @RequestMapping(method = PUT, value = "/{productId}", consumes = APPLICATION_JSON_VALUE)
    public Product updateProduct(String productId, Product product) {
        // TODO implement
        throw new UnsupportedOperationException();
    }

}        

ergibt diese RFC-7807-Fehlermeldungen:

POST /products/123 HTTP/1.1
Content-Type: application/json
 
{}
HTTP/1.1 405 Method Not Allowed
Allow: GET
Content-Type: application/problem+json
 
{
    "title": "Method Not Allowed",
    "status": 405,
    "detail": "POST not supported"
}

Zudem kann man sehr einfach Fehlermeldungen anreichern und ggf. um Stacktraces erweitern.

C# Asp.net Core

Asp.net Core ab 2.2 unterstützt RFC 7807 nativ für Validierungsfehler (4xx), aber nicht für andere Fehler. Support dafür bietet das NuGet Hellang.Middleware.ProblemDetails, siehe z. B. diese Anleitung für Details.

Python

Es gibt für die mir bekannten Python-Frameworks leider keine Bibliotheken, um automatisch „Problem-JSON“ zurückzugeben. Da FastApi, Flask und AioHttp allerdings globale Exceptionhandler unterstützen, ist es einfach, RFC 7807 nachzurüsten. Für Django gibt es das Projekt drf-problems, das ich aber noch nicht eingesetzt habe.

Fazit

Für Projekte, die noch keine standardisierte Fehler haben, bietet es sich sehr an, direkt RFC 7807 Problem Details zu nutzen – bilden diese doch einen sinnvollen Basissatz ab. Zudem ist abzusehen, dass diverse Librarys in Zukunft Problem Details nativ unterstützen werden, so z. B. Refit für C#.

Artikel von Christian Sauer

Cloud

Schnelles Entwickeln mit Kubernetes in Azure

Weitere Inhalte zu API-Management

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.