CodeGym /Kurse /Java Server /Einheitlicher ErrorResponse für HTTP-API

Einheitlicher ErrorResponse für HTTP-API

Java Server
Level 26, Lektion 1
Verfügbar

1. API-Fehler als Teil des Vertrags

Nach der Validierungsgrenze wird schnell ein neuer Schmerz deutlich: Es gibt bereits mehrere negative Szenarien, und lokale write...Error(...)-Aufrufe beginnen sich über die Handler auszubreiten. An einem einzelnen Endpunkt ist das noch tolerierbar, aber im gesamten API nicht mehr. Also sollten wir den Fehler als Teil des Vertrags anerkennen und ein einziges kanonisches Schema dafür erstellen: Status und Body werden zentral bestimmt, nicht als lose Ansammlung von Sonderfällen. Das werden wir jetzt tun.

Wenn Sie ein lokales HTTP-API schreiben, ist es sehr leicht, in den Ansatz zu verfallen: „Wenn etwas schiefgeht, schreiben wir einfach Text als Antwort und machen weiter.“ Auf den ersten Blick scheint das normal: Sie sehen den Fehler in Postman, also funktioniert alles. Aber schon nach ein paar Tagen werden Sie Ihr eigenes API hassen, weil verschiedene Endpunkte unterschiedliche Fehlerformate, verschiedene Felder und unterschiedliche Nachrichtenstile zurückgeben – und jeder Client (sogar Ihr eigener Postman) wird zu einer archäologischen Expedition, um zu entschlüsseln, „was der Server sagen wollte“.

Aus Clientsicht ist ein Fehler kein Zufall, sondern ein vorhersehbares Szenario. Der Client muss mindestens drei Dinge verstehen: was passiert ist (Problemkategorie), warum es passiert ist (kurze Erklärung) und was man tun kann (Details, falls vorhanden). Und hier entsteht die Idee: Wir erstellen einen einheitlichen JSON-Vertrag für Fehler, der genauso verbindlich ist wie der Vertrag für erfolgreiche Antworten. Dann antwortet jeder Endpunkt in jeder negativen Situation auf die gleiche Weise: Der Client kann immer errorCode abrufen, message lesen und bei Bedarf details anzeigen.

Wichtige Nuance: Ein einheitlicher Error-Response bedeutet nicht, dass wir alles in einen 500 verwandeln. Im Gegenteil, er macht den Unterschied zwischen 400 (schlechte Anfrage), 404 (Ressource nicht gefunden), 409 (Konflikt), 500 (interner Fehler) sichtbar. Wir vermischen diese Status nicht zu einem Brei, sondern verpacken sie elegant in eine einheitliche Form.

2. Minimaler ErrorResponse: 3 Felder

Ein einheitliches Fehlerformat sollte einfach genug sein, dass Sie es tatsächlich überall verwenden, nicht nur „in der Theorie“. In unserem Kurs legen wir eine minimale Form fest: errorCode, message, details. Das ist kein „idealer Weltstandard“, sondern ein funktionales Minimum, das einem Lehr-API ohne Frameworks und separate Infrastruktur bestens dient. Es bringt Vorhersagbarkeit, wird aber nicht zur Bürokratie.

Beginnen wir mit dem DTO selbst. Am besten als record im Paket common.error, da es wirklich ein „allgemeines“ Modell für das gesamte API ist.

package com.example.readlater.common.error;

import java.util.List;

// Einheitliches DTO für Fehler: gleiche Antwortstruktur an jedem Endpunkt.
public record ErrorResponse(
        // Maschinenlesbarer Fehlercode: wird von Clients für Verzweigungen in der Logik benötigt (UI/SDK).
        String errorCode,
        // Menschenlesbare Nachricht: kann dem Benutzer angezeigt werden (ohne technische Details).
        String message,
        // Liste von Präzisierungen: besonders nützlich für Validierung (mehrere Fehler auf einmal).
        List<String> details
) {
}

Die Bedeutung der Felder ist wie folgt.

errorCode ist ein kurzer „maschinenlesbarer“ Code. Er wird nicht nur von Menschen, sondern auch von Client-Programmen benötigt: Die UI kann z. B. anhand von VALIDATION_ERROR Felder hervorheben und bei NOT_FOUND „Objekt nicht gefunden“ anzeigen. Wichtig: errorCode ersetzt nicht den HTTP-Status, er ergänzt ihn.

message ist eine kurze, menschliche Nachricht ohne unnötige Technik. Sie kann dem Benutzer angezeigt werden (in unserem Lehr-API ist das überhaupt der Hauptzweck).

details ist eine Liste von Präzisierungen. Sie ist besonders nützlich für die Validierung: Hier sammeln wir „was genau nicht stimmt“. Für 404 oder 500 wird details oft eine leere Liste sein.

Um ein Gefühl dafür zu bekommen, wie das aussieht, ist es nützlich, die Ziel-JSON-Antworten einmal zu „sehen“.

Beispiel für einen Validierungsfehler (400):

{
  "errorCode": "VALIDATION_ERROR",
  "message": "Anfrage hat die Prüfung nicht bestanden",
  "details": [
    "Feld title ist erforderlich",
    "Feld status muss eines sein: PLANNED, IN_PROGRESS, FINISHED"
  ]
}

Beispiel für „Nicht gefunden“ (404):

{
  "errorCode": "NOT_FOUND",
  "message": "Leselisten-Element nicht gefunden",
  "details": []
}

Beispiel für einen Konflikt (409), z. B. wegen Eindeutigkeit von externalId:

{
  "errorCode": "CONFLICT",
  "message": "externalId wird bereits von einem anderen Element verwendet",
  "details": [
    "externalId=OL12345M"
  ]
}

Beispiel für einen internen Fehler (500):

{
  "errorCode": "INTERNAL_ERROR",
  "message": "Interner Fehler",
  "details": []
}

Beachten Sie eine „stille Disziplin“: Wir fügen keinen Stack-Trace in die Antwort ein, geben keine internen Klassennamen aus und liefern keine „zufälligen Ausnahmetexte“ nach außen. Der Client muss nicht wissen, dass Sie eine NullPointerException haben (auch wenn man sich manchmal gerne verraten möchte). Der Client braucht einen vorhersehbaren Vertrag, und Sie brauchen die Diagnose in den Logs.

3. Problemkategorien und Status 400/404/409/500

Jetzt müssen wir uns darauf einigen, welche „Problemtypen“ es in unserem API überhaupt gibt und wie sie in HTTP-Status umgesetzt werden. Wichtig ist, das Leben nicht zu kompliziert zu machen: Unser Projekt ist ein Lehrprojekt und ohne Datenbank, aber wir wollen dennoch die reale HTTP-Semantik wahrheitsgetreu abbilden. Daher verwenden wir einen Basissatz: 400, 404, 409, 500. Mit diesem Set trainiert man hervorragend das Denken in „Anfragefehler vs. Serverfehler“ und „schlechte Daten vs. fehlende Ressource vs. Datenkonflikt“.

Es ist hilfreich, die Entsprechung in einer Tabelle festzuhalten. Das ist fast wie eine Spickzettel fürs Gehirn: Problem erkannt – sofort den richtigen Status und den passenden errorCode parat haben.

Situation (Bedeutung) Beispiel aus ReadLater API HTTP-Status errorCode
Client hat schlechte Daten gesendet (Request besteht Prüfung nicht) leerer title, unbekannter status, ungültiges JSON 400 VALIDATION_ERROR / MALFORMED_JSON
Ressource nicht gefunden GET /api/v1/reading-list/999, aber solche ID existiert nicht 404 NOT_FOUND
Konflikt mit aktuellen Daten externalId muss eindeutig sein, ist aber bereits vergeben 409 CONFLICT
Interner Serverfehler unerwartete Ausnahme, Bug, Serialisierungsfehler 500 INTERNAL_ERROR

Hier gibt es einen wichtigen Gedanken: Der gleiche HTTP-Status 400 kann unterschiedliche Dinge bedeuten. Zum Beispiel „JSON konnte nicht geparst werden“ und „JSON wurde geparst, aber title ist leer“ – das sind verschiedene Probleme, aber beide fallen unter „schlechte Anfrage“. Daher unterscheiden wir sie über den errorCode: MALFORMED_JSON und VALIDATION_ERROR. Das macht die Fehler sowohl HTTP-korrekt als auch clientfreundlich.

Und noch eine Regel, die banal klingt, aber ständig verletzt wird: 500 ist nicht „jeder unangenehme Fall“, sondern genau ein unerwartetes Problem auf Serverseite. Wenn eine Anfrage die Prüfung nicht besteht, ist das kein 500, auch wenn es Ihnen sehr leid tut.

4. Ausnahmen als „Problemsprache“ im Code

An diesem Punkt haben wir eine Aufgabe: Innerhalb des Java-Codes müssen wir die Problemtypen unterscheiden können, um später korrekt den Status und den Antwort-Body auszuwählen. Der verständlichste Weg für ein Lehrprojekt ist es, einige eigene Ausnahmen einzuführen. Nicht „der Schönheit wegen“, sondern damit der Handler sagen kann: „Aha, das ist ein Konflikt, und das ist nicht gefunden“.

Beginnen wir mit einem Satz minimaler Ausnahmen. Es ist praktisch, sie in com.example.readlater.common.error zu halten, damit sie sowohl für Lese- als auch für Schreiboperationen und andere Teile der Anwendung zugänglich sind.

package com.example.readlater.common.error;

import java.util.List;

// Ausnahme für Validierungsfehler: speichert eine Liste aller Probleme, nicht nur eines.
public class ValidationException extends RuntimeException {
    private final List<String> details;

    public ValidationException(List<String> details) {
        // Die Nachricht geben wir ohnehin nicht nach außen, daher speichern wir nur die details.
        this.details = details;
    }

    // Liefert die Details, um ErrorResponse.details zu füllen.
    public List<String> details() {
        return details;
    }
}

Und zwei „einfache“ Ausnahmen ohne Details:

package com.example.readlater.common.error;

// 404: Ressource nicht gefunden (gültige Anfrage, aber keine Daten vorhanden).
public class NotFoundException extends RuntimeException {
    public NotFoundException(String message) {
        // message ist nützlich für Logs/Diagnose, muss aber nicht zwingend an den Client gehen.
        super(message);
    }
}
package com.example.readlater.common.error;

// 409: Konflikt mit aktuellen Daten (z. B. Geschäftsregel zur Eindeutigkeit verletzt).
public class ConflictException extends RuntimeException {
    public ConflictException(String message) {
        // message ist nützlich für Logs/Diagnose, muss aber nicht zwingend an den Client gehen.
        super(message);
    }
}

Wo treten sie in unserem Projekt auf?

Validierung (ValidationException) tritt an der API-Grenze auf, wenn wir das DTO geprüft und Probleme gefunden haben. In der letzten Vorlesung haben wir List<String> details zurückgegeben. Jetzt ist es praktisch, es so zu machen: Wenn die Liste nicht leer ist – werfen wir eine ValidationException, und der einheitliche Fehlermechanismus erledigt den Rest.

Zum Beispiel im Handler:

List<String> details = validator.validateForCreate(request);
if (!details.isEmpty()) {
    throw new ValidationException(details);
}

Eine NotFoundException wird meist von der Service-Schicht geworfen, wenn eine Ressource bei korrekter id nicht gefunden wird. Das ist wichtig: Eine gültige ID, aber eine nicht vorhandene Ressource – das ist kein 400, sondern ein 404.

Beispiel (Ausschnitt aus dem Service):

ReadingListItem item = repository.findById(id)
        .orElseThrow(() -> new NotFoundException("Item not found: id=" + id));

Eine ConflictException werfen wir bei einer Geschäftsregel wie „externalId muss eindeutig sein“. Das ist kein Formatfehler in der Anfrage, denn die Anfrage ist vom Format her in Ordnung. Es ist genau ein Konflikt mit dem aktuellen Zustand des Dienstes.

Beispiel (bedingter Service-Ausschnitt):

if (repository.existsByExternalId(request.externalId(), id)) {
    throw new ConflictException("externalId already used");
}

So erzielen wir einen wichtigen Effekt: Der Code unterscheidet die Bedeutung von Problemen, noch bevor wir mit dem Schreiben der HTTP-Antwort beginnen. Und damit kann die Übersetzung in 400/404/409/500 zentral und ohne Copy-Paste erfolgen.

5. ErrorMapper: Status und ErrorResponse

Wenn Sie keinen zentralen Error-Mapping-Mechanismus haben, werden Sie in jedem Endpunkt das Gleiche schreiben: catch, Statusauswahl, JSON-Zusammenbau, in die Antwort schreiben. Am dritten Tag wird daraus ein tristes Copy-Paste, am fünften eine Desynchronisation der Verträge und am zehnten „niemand weiß, welches Fehlerformat wo verwendet wird“. Deshalb machen wir eine kleine, aber zentrale Klasse: ErrorMapper.

Sie sollte kein „Mini-Framework“ sein. Es ist buchstäblich eine „Entsprechungstabelle im Code“. Normalerweise reicht eine Datei.

package com.example.readlater.common.error;

public class ErrorMapper {

    public int statusFor(Exception ex) {
        // Zuerst – erwartete (vertragliche) Client-/Domänenfehler.
        if (ex instanceof ValidationException) return 400;
        if (ex instanceof NotFoundException) return 404;
        if (ex instanceof ConflictException) return 409;

        // Alles andere betrachten wir als unerwarteten Serverfehler.
        return 500;
    }
}

Und nun der Fehler-Body. Bei Validierung nehmen wir die details, bei den anderen geben wir eine leere Liste zurück. Für message sollten besser stabile Phrasen verwendet werden als die Texte der Ausnahmen (sonst „tanzen“ die Nachrichten und irgendwann geben Sie versehentlich zu viele Informationen preis).

package com.example.readlater.common.error;

import java.util.List;

public class ErrorMapper {

    public ErrorResponse bodyFor(Exception ex) {
        // Validierung: gib die Liste aller Probleme zurück, damit der Client alles auf einmal korrigieren kann.
        if (ex instanceof ValidationException vex) {
            return new ErrorResponse("VALIDATION_ERROR", "Anfrage hat die Prüfung nicht bestanden", vex.details());
        }

        // 404/409: Details bleiben in den Logs, der Client erhält einen stabilen Vertrag.
        if (ex instanceof NotFoundException) {
            return new ErrorResponse("NOT_FOUND", "Ressource nicht gefunden", List.of());
        }
        if (ex instanceof ConflictException) {
            return new ErrorResponse("CONFLICT", "Datenkonflikt", List.of());
        }

        // 500: maximal „langweilige“ Antwort ohne interne Details.
        return new ErrorResponse("INTERNAL_ERROR", "Interner Fehler", List.of());
    }
}

Ja, hier steckt eine kleine „Frechheit“: NotFoundException speichert eine message, aber wir zeigen sie dem Client nicht. Das ist beabsichtigt. In den Logs können Sie die genaue id sehen, dem Client reicht meist „Ressource nicht gefunden“. In einem Lehrprojekt ist das besonders nützlich: Wir trainieren die Grenze zwischen „was gehört zum Client“ und „was gehört in die Logs“.

Jetzt fehlt noch die letzte Verbindung: Aus einer Ausnahme Status + Body ermitteln und JSON schreiben. Das heißt, wir brauchen einen „Standardausgang“ in der Handler-Schicht.

6. Fehlerbehandlung im Handler: ein einziger try/catch

Da wir mit HttpServer arbeiten, haben wir keinen globalen Exception-Handler „aus der Box“. Daher ist der realistischste Lehrpfad, einen einzigen try/catch auf Handler-Ebene (oder zumindest in der Haupt-handle()-Methode) zu machen, der Ausnahmen abfängt und in einen ErrorResponse verwandelt. Das ist wieder kein Framework, sondern eine normale Ingenieursgewohnheit: Sie legen vorher fest, wo die „normale Logik“ endet und der „Notausstieg“ beginnt.

Stellen wir uns vor, wir haben bereits ein Utility JsonWriter (oder eine Methode writeJson) aus den vergangenen Tagen, das jedes Objekt über ObjectMapper in JSON schreibt. Im Handler verwenden wir einfach den ErrorMapper.

Bei HttpExchange-Operationen gibt es oft checked exceptions, daher stört ein einfaches Runnable hier schnell. Praktischer ist es, gleich ein kleines funktionales Interface für die Handler-Aktion zu definieren.

import com.example.readlater.common.error.ErrorMapper;
import com.example.readlater.common.error.ErrorResponse;
import com.sun.net.httpserver.HttpExchange;
import org.slf4j.Logger;

import java.io.IOException;

@FunctionalInterface
interface ExchangeAction {
    void run() throws Exception;
}

class ReadingListHttpHandler {
    private final ErrorMapper errorMapper;
    private final JsonWriter jsonWriter;
    private final Logger logger;

    void handleSafely(HttpExchange exchange, ExchangeAction action) throws IOException {
        try {
            // Innerhalb von action wird die „normale“ Endpunkt-Logik ausgeführt.
            action.run();
        } catch (Exception ex) {
            // Details – in den Logs (hier landet der Stack-Trace).
            logger.error("Request failed", ex);

            // Und dem Client – den Vertrag: Status + ErrorResponse.
            int status = errorMapper.statusFor(ex);
            ErrorResponse body = errorMapper.bodyFor(ex);

            // Einheitliche JSON-Serialisierung für alle Fehler.
            jsonWriter.writeJson(exchange, status, body);
        }
    }
}

Hier gibt es eine sehr wichtige Kleinigkeit: Wir protokollieren die Ausnahme immer als technisches Ereignis, geben dem Client aber den Vertrag zurück. So haben Sie beide Seiten des Backend-Lebens: Der Client sieht nicht die Interna, aber Sie tappen nicht im Dunkeln.

Jetzt kann die handle()-Methode etwa so aussehen:

import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;

public void handle(HttpExchange exchange) throws IOException {
    handleSafely(exchange, () -> {
        // Anfragekontext extrahieren und routen.
        String method = exchange.getRequestMethod();
        String path = exchange.getRequestURI().getPath();
        route(method, path, exchange);
    });
}

Und in route(...) können Sie dann beruhigt eine NotFoundException, ValidationException oder ConflictException werfen, ohne jedes Mal darüber nachdenken zu müssen, „welcher Status gesetzt werden muss“. Das ist das Ziel: Anwendungscode von der mühsamen Umwandlung von Fehlern in HTTP zu trennen.

Ab diesem Punkt hat das API bereits eine funktionierende Baseline: Endpunkte werfen sinnvolle Ausnahmen, und der Handler übersetzt sie genau einmal in Status + ErrorResponse.

7. Zwei Arten von 400: MALFORMED_JSON und DTO-Validierung

Mit ValidationException ist alles klar: Das DTO wurde geparst, aber der Inhalt ist schlecht. Was aber tun, wenn das JSON überhaupt nicht geparst werden konnte? JSON, das nicht geparst werden konnte, ist eine andere Art von Problem. Und auch dieses muss im einheitlichen Error-Vertrag leben, sonst gibt ein Endpunkt einen JSON-Fehler zurück, ein anderen einen „Plain Text: invalid json“, und Sie sind wieder im Chaos.

Am einfachsten: Die Jackson-Ausnahme beim Lesen des Bodys abfangen und eine ValidationException werfen? Möglich, aber dann verlieren Sie die Unterscheidung, und der Client versteht nicht genau, was passiert ist. Daher ist es besser, einen separaten errorCode für malformed JSON einzuführen.

Die einfachste Variante ist, keine separate Ausnahme zu erzeugen, sondern im catch direkt in eine ValidationException mit Details „Ungültiges JSON“ umzuwandeln. Aber mir gefällt ein etwas expliziterer Ansatz besser: Eine eigene MalformedJsonException einzuführen (die wird sowieso klein sein).

package com.example.readlater.common.error;

// 400: Der Request-Body ist kein gültiges JSON (wir haben noch nicht einmal die DTO-Validierung erreicht).
public class MalformedJsonException extends RuntimeException {
    public MalformedJsonException() {
        super("Malformed JSON");
    }
}

Und erweitern Sie den ErrorMapper (ich zeige nur den wesentlichen Teil, um den Code nicht auseinanderzuziehen):

public int statusFor(Exception ex) {
    // Ungültiges JSON und DTO-Validierungsfehler – verschiedene Ursachen, aber gleicher Status (400).
    if (ex instanceof MalformedJsonException) return 400;
    if (ex instanceof ValidationException) return 400;
    if (ex instanceof NotFoundException) return 404;
    if (ex instanceof ConflictException) return 409;
    return 500;
}
public ErrorResponse bodyFor(Exception ex) {
    // Für JSON-Parsing geben wir einen eigenen errorCode zurück, damit der Client die Ursache versteht.
    if (ex instanceof MalformedJsonException) {
        return new ErrorResponse("MALFORMED_JSON", "Ungültiges JSON", List.of());
    }
    // restliche Zweige wie zuvor...
    return new ErrorResponse("INTERNAL_ERROR", "Interner Fehler", List.of());
}

Jetzt sieht das Lesen des Bodys sauber aus: Zuerst versuchen wir zu parsen, und wenn Jackson fehlschlägt, bauen wir keinen Text in die Antwort, sondern werfen eine verständliche Ausnahme, die dann vom allgemeinen Mechanismus behandelt wird.

import com.fasterxml.jackson.core.JsonProcessingException;

CreateReadingItemRequest readCreateRequest(String body) {
    try {
        // Versuch, JSON in DTO zu parsen.
        return objectMapper.readValue(body, CreateReadingItemRequest.class);
    } catch (JsonProcessingException ex) {
        // Den Jackson-Exception-Text nicht nach außen geben: wir wandeln in unseren Vertragstyp um.
        throw new MalformedJsonException();
    }
}

Die Bedeutung dieses kleinen Schrittes ist enorm: Jetzt wird jeder Endpunkt, der JSON liest, „kaputtes JSON“ auf die gleiche Weise behandeln. Und Sie werden nicht durch das Projekt rennen und suchen müssen, „wo wir versehentlich einen Plain-Text zurückgegeben haben“.

Das ist bereits eine Baseline, die für alle JSON-Endpunkte einheitlich sein sollte.

8. 500: Antwort an den Client und Logs getrennt

Wenn Ihr Code unerwartet abstürzt, möchte man es gerne „wie im Film“ machen: dem Client den Stack-Trace zurückgeben, weil das „einfacher zu debuggen“ ist. Aber das ist genau der Moment, in dem die Backend-Gewohnheiten siegen sollten. Der Client ist nicht Ihr Debugger. Der Client sollte eine stabile und sichere Nachricht erhalten, und die Diagnosedetails gehören in die Logs. Selbst in einem Lehrprojekt ist das wichtig, weil es den richtigen Reflex formt.

Daher machen wir für 500 eine maximal langweilige Antwort: errorCode=INTERNAL_ERROR, message=Interner Fehler, details=[]. Das ist alles. Kein java.lang.NullPointerException: .... Wenn Sie debuggen müssen, öffnen Sie die Logs – wir haben sie bereits im Modul über SLF4J + Logback angeschlossen.

Beispiel dafür, wie das in handleSafely aussieht (wir haben es schon geschrieben, aber jetzt betrachten wir es mit den Augen einer Betriebsperson):

catch (Exception ex) {
    logger.error("Request failed", ex); // Stack-Trace hier, nicht in der Response
    int status = errorMapper.statusFor(ex);
    ErrorResponse body = errorMapper.bodyFor(ex);
    jsonWriter.writeJson(exchange, status, body);
}

Feinheit: Nicht alle Fehler müssen als error geloggt werden. Validierung ist eine normale Situation im API-Leben, man kann sie auf warn loggen (oder gar nicht, wenn es Rauschen ist). Aber unerwartete Ausnahmen – ja, das ist error, denn das ist Ihr Bug oder Ihr Fehler.

Und noch ein praktischer Punkt: Loggen Sie nicht den gesamten Request-Body „für alle Fälle“. In unserem Projekt sind die Payloads klein, aber die Angewohnheit, „alles Mögliche zu loggen“, führt in der realen Welt schnell zu Datenlecks und riesigen Protokollen. Besser ist es, einen kurzen Kontext zu loggen (Methode, Pfad, Status, Fehlertyp) anstatt den gesamten Request-Inhalt.

9. Typische Fehler: Einheitlicher Error-Response

Fehler Nr. 1: Unterschiedliche Endpunkte geben unterschiedliche Fehlerformate zurück.
Sehr oft fängt man schön an: Ein Endpunkt gibt ErrorResponse zurück, aber irgendwo taucht ein „temporäres“ exchange.sendResponseHeaders(400, ...) mit Plain-Text auf, und das bleibt dann so. Nach einer Woche sieht die Postman-Collection aus wie „Rate das Antwortformat“. Das heilt man durch Disziplin: Jede negative Antwort – über den einheitlichen ErrorResponse und einen einzigen JSON-Schreibmechanismus.

Fehler Nr. 2: Der Stack-Trace oder die technische Fehlermeldung der Ausnahme wandert in den Response-Body.
Zuerst scheint das praktisch. Dann merkt man plötzlich, dass der Client interne Details sieht, Klassennamen, Pfadstücke und manchmal sogar Anfragedaten. Der richtige Ansatz ist einfach: Technische Details in logger.error("...", ex), dem Client eine kurze stabile Nachricht und errorCode, ohne unnötige „Herzensangelegenheiten“.

Fehler Nr. 3: 500 wird als „universeller Standardstatus“ verwendet.
Wenn Sie jede Ausnahme abfangen und immer mit 500 antworten, verlieren Sie die semantische Genauigkeit des API. Der Client versteht nicht, ob er schuld ist (schlechte Anfrage) oder der Server kaputt ist. In unserem Projekt ist das besonders bei der Validierung spürbar: Ein leerer title ist ein 400, kein 500. Auch 404 und 409 sollten ihr eigenes Leben haben, sonst trainieren Sie nicht das HTTP-Denken, für das wir diesen Server überhaupt geschrieben haben.

Fehler Nr. 4: errorCode vermehrt sich unkontrolliert und wird zu einem „Wörterbuch mit 200 Einträgen“.
Manchmal möchte man den errorCode superdetailliert machen: einen eigenen Code für jedes Niesen. In einem kleinen API führt das dazu, dass die Codes instabil werden und ihre Bedeutung verlieren. Besser ist es, einen kurzen, festen Satz zu halten wie MALFORMED_JSON, VALIDATION_ERROR, NOT_FOUND, CONFLICT, INTERNAL_ERROR, und die Details in details zu packen.

Fehler Nr. 5: Validierungsfehler „stoppen am ersten Feld“ und geben nicht die vollständige Liste der Probleme zurück.
Wenn Sie Felder prüfen und sofort beim ersten Fehler eine Ausnahme werfen, wird der Client die Anfrage „einen Fehler nach dem anderen“ reparieren müssen, wie in einem schlechten Spiel. Die Validierung an der API-Grenze sollte alle Probleme auf einmal sammeln, damit der Client die vollständige Liste erhält und alles auf einmal korrigieren kann. Genau dafür haben wir details: List<String>.

1
Aufgabe
Java Server, Level 26, Lektion 1
Gesperrt
`ErrorMapperDemo` fur einheitliches `ErrorResponse`
`ErrorMapperDemo` fur einheitliches `ErrorResponse`
1
Aufgabe
Java Server, Level 26, Lektion 1
Gesperrt
Gemeinsames `handleSafely(...)` für einen lokalen Endpoint
Gemeinsames `handleSafely(...)` für einen lokalen Endpoint
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION