API-Fehlerformate mit RFC 7807 standardisieren: Problem Details in PHP
AI generated
<?php
8.4
PHP · API Design · RFC 7807 · Error Handling
API-Fehlerformate mit RFC 7807 standardisieren
Problem Details statt uneinheitlicher Fehlerantworten

RFC 7807 definiert ein einziges, vorhersagbares JSON-Format für Fehlerantworten: type, title, status, detail und instance. Dieser Artikel zeigt, wie eine PHP-API dieses Format konsequent umsetzt, von der Problem-Details-Klasse über den globalen Exception-Handler bis zu fachlichen Validierungsfehlern mit eigenen Erweiterungsfeldern.

17 Min. Lesezeit RFC 7807 · Problem Details · Error Handling PHP 8.4

1. Warum uneinheitliche Fehlerformate ein Problem sind

Die meisten PHP-APIs entwickeln ihr Fehlerformat organisch: der erste Endpunkt gibt {"error": "not found"} zurück, der zweite {"message": "invalid input"}, ein dritter {"errors": ["field required"]}. Nach einem Jahr Entwicklung hat jedes Team im Projekt sein eigenes Fehlerformat etabliert, und jeder Client muss für jeden Endpunkt eine eigene Fehlerbehandlung schreiben. Genau dieses Problem löst RFC 7807, das offizielle Format für Problem Details in HTTP-APIs.

Ohne ein einheitliches Fehlerformat entsteht in wachsenden PHP-Projekten ein stiller, aber teurer Wartungsaufwand: jede neue Fehlerquelle bekommt eine neue Ad-hoc-Struktur, jedes Frontend-Team muss zusätzliche Sonderfälle im eigenen Error-Handling abbilden. RFC 7807 beendet diese Fragmentierung, indem es ein einziges, erweiterbares JSON-Schema für alle Fehlerantworten definiert, unabhängig davon, ob der Fehler eine Validierung, eine Autorisierung oder ein internes Serverproblem betrifft.

Der praktische Vorteil zeigt sich vor allem bei mehreren Konsument-Teams: sobald eine API mit RFC 7807 antwortet, kann jedes Client-Team dieselbe generische Fehlerbehandlungslogik verwenden, statt für jeden Endpunkt eigene Parsing-Regeln zu schreiben. Das reduziert Integrationsaufwand und macht Fehlerbehandlung selbst zu einem stabilen, dokumentierten Teil des API-Vertrags.

2. Was RFC 7807 vorschreibt

RFC 7807, offiziell "Problem Details for HTTP APIs", definiert ein JSON-Objekt mit fünf standardisierten Feldern, die zusammen einen Fehler eindeutig beschreiben. Die Spezifikation legt bewusst nicht fest, welche fachlichen Zusatzinformationen ein Fehler enthalten darf, sie definiert nur den gemeinsamen Rahmen und den passenden Media-Type application/problem+json. Diese Offenheit macht RFC 7807 flexibel genug für simple 404-Antworten und komplexe Validierungsfehler mit mehreren Feldfehlern gleichzeitig.

Ein zentraler Gedanke der Spezifikation: der HTTP-Statuscode allein reicht selten aus, um einen Fehler für Menschen und Maschinen gleichermaßen verständlich zu machen. Ein 422 sagt nichts darüber aus, welches Feld ungültig war, ein 403 sagt nichts darüber aus, welche Berechtigung fehlt. RFC 7807 schließt genau diese Lücke zwischen Statuscode und tatsächlicher Fehlerursache, ohne die HTTP-Semantik selbst zu verändern.

3. Die Kernfelder: type, title, status, detail, instance

Das Feld type ist eine URI, die den Fehlertyp identifiziert, standardmäßig about:blank, wenn kein spezifischerer Typ existiert. Das Feld title ist eine kurze, für Menschen lesbare Zusammenfassung des Fehlertyps und sollte sich zwischen mehreren Vorkommen desselben Fehlertyps nicht ändern. Das Feld status spiegelt den HTTP-Statuscode aus der Antwort wider, wird aber redundant im Body mitgeliefert, damit Logging-Systeme und Clients ihn auch ohne Zugriff auf den HTTP-Header auswerten können.

Das Feld detail enthält die konkrete, für diesen speziellen Vorfall spezifische Fehlerbeschreibung, im Gegensatz zu title, das generisch bleibt. Das Feld instance ist eine URI, die den konkreten Vorfall identifiziert, häufig genutzt für eine Request-ID oder einen Correlation-Identifier, mit dem sich der Fehler später in Logs wiederfinden lässt. Diese fünf Felder bilden zusammen das Fundament jeder RFC 7807-Antwort, weitere Felder dürfen laut Spezifikation beliebig ergänzt werden.


{
  "type": "https://api.example.com/problems/insufficient-stock",
  "title": "Insufficient stock",
  "status": 409,
  "detail": "Only 2 units of SKU MS-4821 are available, but 5 were requested.",
  "instance": "/orders/attempts/9f3a2c1e"
}

4. Erweiterungsfelder für fachliche Validierungsfehler

Die eigentliche Stärke von RFC 7807 zeigt sich bei Validierungsfehlern mit mehreren betroffenen Feldern. Die Spezifikation erlaubt beliebige Erweiterungsfelder neben den fünf Kernfeldern, in der Praxis hat sich dafür ein Feld namens errors etabliert, eine Liste von Objekten mit Feldname und zugehöriger Fehlermeldung. Dieses Muster ist nicht Teil der offiziellen Spezifikation, aber in der Praxis quasi zum inoffiziellen Standard geworden, unter anderem durch Frameworks wie Spring und ASP.NET Core, die es genauso implementieren.

Wichtig ist, dass Erweiterungsfelder dokumentiert und über alle Endpunkte einer PHP-API hinweg konsistent benannt werden. Wer in einem Endpunkt errors und im nächsten fieldErrors nennt, verspielt genau den Konsistenzgewinn, den RFC 7807 eigentlich verspricht. Eine gemeinsame Basisklasse für alle Problem-Details-Antworten stellt sicher, dass Erweiterungsfelder projektweit einheitlich heißen und strukturiert sind.


{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Validation failed",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "instance": "/customers/register/attempts/7c2e1a90",
  "errors": [
    { "field": "email", "message": "Must be a valid email address." },
    { "field": "password", "message": "Must be at least 12 characters long." }
  ]
}

5. Eine ProblemDetails-Klasse in PHP bauen

Der praktische Einstieg in RFC 7807 beginnt mit einem einzigen, unveränderlichen Value Object, das die fünf Kernfelder plus beliebige Erweiterungsfelder kapselt. Diese Klasse übernimmt auch die Serialisierung nach JSON und die Erzeugung der passenden HTTP-Response mit korrektem Statuscode und Content-Type, sodass Controller-Code nur noch ein ProblemDetails-Objekt bauen und zurückgeben muss.

Die Konstruktion über statische Fabrikmethoden wie ProblemDetails::notFound() oder ProblemDetails::validationFailed() hält den Aufrufcode kurz und verhindert, dass an verschiedenen Stellen im Projekt leicht unterschiedliche Feldnamen oder Statuscodes für denselben logischen Fehler verwendet werden.


<?php

declare(strict_types=1);

/**
 * Immutable RFC 7807 Problem Details value object.
 */
final class ProblemDetails
{
    /**
     * @param array<int, array{field: string, message: string}> $errors
     */
    private function __construct(
        private readonly string $type,
        private readonly string $title,
        private readonly int $status,
        private readonly string $detail,
        private readonly ?string $instance = null,
        private readonly array $errors = [],
    ) {
    }

    public static function validationFailed(array $errors, ?string $instance = null): self
    {
        return new self(
            type: 'https://api.example.com/problems/validation-error',
            title: 'Validation failed',
            status: 422,
            detail: 'One or more fields failed validation.',
            instance: $instance,
            errors: $errors,
        );
    }

    public static function notFound(string $resource, ?string $instance = null): self
    {
        return new self(
            type: 'https://api.example.com/problems/not-found',
            title: 'Resource not found',
            status: 404,
            detail: "The requested {$resource} could not be found.",
            instance: $instance,
        );
    }

    public static function internalServerError(?string $instance = null): self
    {
        return new self(
            type: 'about:blank',
            title: 'Internal Server Error',
            status: 500,
            detail: 'An unexpected error occurred while processing the request.',
            instance: $instance,
        );
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        $payload = [
            'type' => $this->type,
            'title' => $this->title,
            'status' => $this->status,
            'detail' => $this->detail,
        ];

        if ($this->instance !== null) {
            $payload['instance'] = $this->instance;
        }

        if ($this->errors !== []) {
            $payload['errors'] = $this->errors;
        }

        return $payload;
    }

    public function status(): int
    {
        return $this->status;
    }
}

6. Ein globaler Exception-Handler für Problem Details

Damit nicht jeder Controller manuell ein ProblemDetails-Objekt bauen muss, lohnt sich ein zentraler Exception-Handler, der fachliche Exceptions automatisch in RFC 7807-Antworten übersetzt. Jede fachliche Exception implementiert dafür eine kleine Schnittstelle mit einer Methode, die das passende ProblemDetails-Objekt liefert, der Handler ruft diese Methode auf und schreibt die Antwort.

Für unerwartete, nicht fachliche Exceptions liefert der Handler eine generische 500-Antwort mit minimalem detail-Text, um keine internen Implementierungsdetails preiszugeben. Dieser zweistufige Ansatz, fachliche Exceptions mit eigenem Problem-Type und alles andere als generischer interner Fehler, ist das Grundmuster für robustes Error-Handling mit RFC 7807 in PHP.


<?php

declare(strict_types=1);

interface ProblemAwareException
{
    public function toProblemDetails(): ProblemDetails;
}

/**
 * Central exception handler translating exceptions into RFC 7807 responses.
 */
final class ProblemDetailsExceptionHandler
{
    public function handle(\Throwable $exception): void
    {
        $problem = $exception instanceof ProblemAwareException
            ? $exception->toProblemDetails()
            : $this->genericServerError();

        http_response_code($problem->status());
        header('Content-Type: application/problem+json');
        echo json_encode($problem->toArray(), JSON_UNESCAPED_SLASHES);
    }

    private function genericServerError(): ProblemDetails
    {
        return ProblemDetails::internalServerError();
    }
}

7. Den Content-Type application/problem+json richtig setzen

Ein häufiger Fehler in PHP-APIs, die RFC 7807 teilweise umsetzen: die Fehlerantwort hat die richtige JSON-Struktur, wird aber weiterhin mit Content-Type: application/json ausgeliefert. Die Spezifikation verlangt explizit application/problem+json, damit Clients zwischen erfolgreichen Antworten und Fehlerantworten allein anhand des Media-Types unterscheiden können, ohne den Statuscode separat prüfen zu müssen.

In der Praxis lohnt es sich, diesen Content-Type zentral im Exception-Handler zu setzen und niemals in einzelnen Controllern zu wiederholen. Middleware-basierte Frameworks bieten dafür meist einen zentralen Error-Responder, der genau einmal im Bootstrapping registriert wird, sodass jede Fehlerantwort der API automatisch den korrekten Media-Type trägt, unabhängig davon, welcher Endpunkt den Fehler ausgelöst hat.

8. Fehlertypen als dereferenzierbare URIs modellieren

Die Spezifikation schlägt vor, dass die type-URI im Idealfall dereferenzierbar ist, also tatsächlich auf eine Seite mit menschenlesbarer Dokumentation zu diesem Fehlertyp zeigt. In der Praxis setzen viele PHP-APIs das nur teilweise um, oft bleibt die URI ein reiner Bezeichner ohne echte Zielseite. Das ist laut Spezifikation zulässig, schöpft aber nicht das volle Potenzial von RFC 7807 aus.

Ein pragmatischer Mittelweg für PHP-Projekte: eine interne Fehlerdokumentationsseite pro Projekt aufsetzen, auf der jeder verwendete Fehlertyp mit Beispielantwort und möglichen Ursachen beschrieben ist. Die type-URI zeigt dann auf diese Seite, Support- und Frontend-Teams können bei einem unbekannten Fehlertyp direkt nachschlagen, statt im Code nach der auslösenden Stelle suchen zu müssen.

9. Ad-hoc-Fehlerformat vs. RFC 7807 im Vergleich

Die folgende Tabelle stellt ein typisches, historisch gewachsenes Ad-hoc-Fehlerformat einer RFC 7807-konformen Antwort gegenüber.

Aspekt Ad-hoc-Format RFC 7807
Konsistenz über Endpunkte Unterschiedlich pro Team/Endpunkt Ein Schema für alle Fehler
Media-Type application/json application/problem+json
Fehlertyp maschinenlesbar Meist nur freier Text type-URI eindeutig identifizierbar
Nachvollziehbarkeit im Log Kein Standard-Feld für Instanz-ID instance-Feld für Correlation-ID
Client-Fehlerbehandlung Pro Endpunkt individuell Generische, wiederverwendbare Logik

Die Umstellung eines Bestandssystems auf RFC 7807 ist selten mit einem Big-Bang-Rewrite verbunden, meist genügt es, den zentralen Exception-Handler und die ProblemDetails-Klasse einzuführen und neue Endpunkte konsequent darauf umzustellen, während bestehende Endpunkte schrittweise nachgezogen werden.

Mironsoft

PHP-API-Design, Error-Handling und Backend-Architektur

Ein einheitliches Fehlerformat für eure PHP-API?

Wir führen RFC 7807 in bestehenden PHP-APIs ein, bauen die ProblemDetails-Klasse, den globalen Exception-Handler und dokumentieren jeden Fehlertyp konsistent für alle Client-Teams.

Fehlerformat-Audit

Bestehende Endpunkte auf uneinheitliche Fehlerformate prüfen

RFC-7807-Einführung

ProblemDetails-Klasse und Exception-Handler produktionsreif aufsetzen

Dokumentation

Fehlertypen dereferenzierbar dokumentieren für Support und Partner

10. Zusammenfassung

RFC 7807 löst ein Problem, das in fast jeder gewachsenen PHP-API auftritt: uneinheitliche, teamspezifische Fehlerformate, die Client-Integrationen unnötig aufwendig machen. Die fünf Kernfelder type, title, status, detail und instance bilden zusammen mit projektweit konsistenten Erweiterungsfeldern wie errors ein vollständiges, erweiterbares Format für jede Art von Fehler, von einfachen 404-Antworten bis zu komplexen Validierungsfehlern mit mehreren betroffenen Feldern.

Die Umsetzung in PHP gelingt am saubersten über ein zentrales ProblemDetails-Value-Object und einen globalen Exception-Handler, der fachliche Exceptions automatisch in die passende RFC 7807-Antwort übersetzt und dabei konsequent den Content-Type application/problem+json setzt. Wer diese Grundlage einmal etabliert, spart sich in jedem neuen Endpunkt die wiederkehrende Diskussion über das richtige Fehlerformat.

RFC 7807 in PHP: Das Wichtigste auf einen Blick

Kernfelder

type, title, status, detail, instance bilden zusammen ein vollständiges, standardisiertes Fehlerobjekt.

Content-Type

application/problem+json muss auf jeder Fehlerantwort gesetzt werden, zentral im Exception-Handler, nie im Controller.

Erweiterung

Ein konsistent benanntes errors-Feld deckt fachliche Validierungsfehler mit mehreren Feldern ab.

Umsetzung

Eine ProblemDetails-Klasse plus zentraler Exception-Handler statt Ad-hoc-Fehlerarrays in jedem Controller.

11. FAQ: API-Fehlerformate mit RFC 7807

1Was ist RFC 7807?
Ein standardisiertes JSON-Format für Fehlerantworten mit type, title, status, detail und instance plus optionalen Erweiterungsfeldern.
2Welcher Content-Type ist Pflicht?
application/problem+json statt application/json, damit Fehlerantworten am Media-Type erkennbar sind.
3Muss die type-URI erreichbar sein?
Nein, aber empfohlen: sie sollte im Idealfall auf eine Dokumentationsseite zum Fehlertyp zeigen.
4Wie mehrere Validierungsfehler abbilden?
Über ein konsistentes errors-Feld mit Feldname und Meldung pro betroffenem Feld.
5Was gehört ins detail-Feld?
Der konkrete, für diesen Vorfall spezifische Sachverhalt, im Gegensatz zum generischen title-Feld.
6Wofür dient instance?
Als URI zur Identifikation des konkreten Vorfalls, meist mit einer Request- oder Correlation-ID für das Logging.
7Wie zentral statt pro Controller umsetzen?
Über eine ProblemDetails-Klasse und einen globalen Exception-Handler, der Fehler automatisch übersetzt.
8Wie mit unerwarteten Exceptions umgehen?
Generische 500-Antwort mit minimalem detail-Text, ohne interne Details wie Stacktraces preiszugeben.
9Braucht es einen Rewrite für RFC 7807?
Nein, schrittweise Einführung über zentralen Handler und neue Endpunkte reicht meist aus.
10Ist errors Teil der Spezifikation?
Nein, informeller Standard. Entscheidend ist projektweite Konsistenz bei Erweiterungsfeldern.