Abschnitt 11 · Lektion 6
Fehlerantworten
Deine API kann inzwischen alles, was eine API können muss. Nur im Fehlerfall redet sie mit vier verschiedenen Zungen, und das merkt man erst, wenn jemand anderes sie benutzt.
Vier Fehler, vier Formen
import express from "express";
// Damit Express nicht zusaetzlich den vollen Stacktrace in die Ausgabe legt.
// Nach den Importen, aber vor express(): dort wird der Wert gelesen.
process.env.NODE_ENV = "test";
const app = express();
// Vier Fehlerquellen, wie sie in einem gewachsenen Projekt nebeneinander
// stehen. Jede fuer sich in Ordnung, zusammen unbenutzbar.
app.get("/buecher/99", (req, res) => res.status(404).send("Buch nicht gefunden"));
app.post("/buecher", (req, res) => res.status(400).json({ fehlerText: "titel fehlt" }));
app.get("/kaputt", (req, res) => {
throw new Error("Datenbank nicht erreichbar");
});
app.listen(3000); Rechts steht der erste Fall. /kaputt in der Adresszeile zeigt den dritten, und den zweiten holst du
im Terminal:
curl -i -X POST http://localhost:3000/buecher
Vier Anfragen, vier Antworten, vier Formate. Ein Satz als Text, ein JSON-Objekt mit einem selbst ausgedachten Feldnamen, und zweimal eine HTML-Seite von Express.
Stell dir vor, du schreibst den Aufrufer dazu. Du musst den Content-Type ansehen, bevor du weißt,
ob du auswerten oder anzeigen kannst. Bei JSON musst du wissen, dass das Feld hier fehlerText
heißt und woanders vielleicht error. Und bei den HTML-Antworten bleibt dir nur der Statuscode.
Die Form der Fehlerantwort ist eine Entwurfsentscheidung, genauso wie die Form der Erfolgsantwort aus Lektion 11.2. Sie wird einmal getroffen und dann überall durchgehalten.
Eine Form für alle
import express from "express";
const app = express();
app.use(express.json());
// Eine Hilfe, die einen Fehler mit Status und Code erzeugt. Mehr braucht es
// nicht: ein Error ist ein gewoehnliches Objekt, an das man etwas hängen kann.
function baueFehler(status, code, meldung, felder) {
const fehler = new Error(meldung);
fehler.status = status;
fehler.code = code;
fehler.felder = felder;
return fehler;
}
app.get("/buecher/:id", (req, res, next) => {
next(baueFehler(404, "nicht_gefunden", `Buch ${req.params.id} gibt es nicht`));
});
app.post("/buecher", (req, res, next) => {
next(
baueFehler(400, "ungueltig", "Die Eingabe ist unvollstaendig", [
{ feld: "titel", meldung: "fehlt oder ist leer" },
])
);
});
app.get("/kaputt", (req, res) => {
throw new Error("Datenbank nicht erreichbar");
});
// Ein Ort, eine Form. Alles darueber muss nur next aufrufen.
app.use((fehler, req, res, next) => {
const status = fehler.status ?? 500;
res.status(status).json({
fehler: {
code: fehler.code ?? "serverfehler",
meldung: status === 500 ? "Unerwarteter Fehler" : fehler.message,
felder: fehler.felder,
},
});
});
app.listen(3000); Dieselben drei Fälle, dieselbe Form. Rechts steht der 404, /kaputt gibt den 500, und der 400 kommt
wieder über das Terminal:
curl -i -X POST http://localhost:3000/buecher
Drei völlig verschiedene Fehler, dieselbe Form: ein Objekt fehler mit code, meldung und, wo es
etwas zu sagen gibt, felder.
Zwei Dinge sind daran wichtig. Erzeugt wird die Form an genau einer Stelle, im Fehler-Handler
aus Lektion 10.5. Die Routen bauen keine Antwort, sondern reichen ihren Fehler an next() weiter.
Wenn du später ein Feld ergänzt, änderst du eine Funktion und nicht dreißig Routen.
Und felder fällt einfach weg, wenn nichts drinsteht. JSON.stringify lässt undefined
aus, deshalb steht bei den beiden anderen Antworten kein leeres Feld herum. Dasselbe Objekt, ohne
dass du zwei Formen bauen musst.
Code und Meldung sind zwei verschiedene Dinge
Der code ist für das Programm, die meldung für den Menschen.
Das klingt nach doppelter Arbeit und spart welche. Ein Aufrufer, der auf code === "nicht_gefunden"
prüft, funktioniert weiter, wenn du die Meldung umformulierst oder übersetzt. Einer, der auf den
Meldungstext prüft, geht bei jeder Textänderung kaputt, und das merkt niemand beim Ändern.
Deshalb: auf den Code reagieren, die Meldung anzeigen. Und niemals umgekehrt.
Was nach außen darf und was nicht
import express from "express";
const app = express();
app.get("/kaputt", (req, res) => {
throw new Error("SELECT * FROM benutzer WHERE email = 'a@b.de' schlug fehl");
});
app.use((fehler, req, res, next) => {
const kennung = "a1b2c3";
// Nach innen alles, mit einer Kennung davor.
console.error(`[${kennung}] ${fehler.message}`);
// Nach aussen ein Code, ein Satz und dieselbe Kennung.
res.status(500).json({
fehler: { code: "serverfehler", meldung: "Unerwarteter Fehler", kennung },
});
});
app.listen(3000); Rechts steht, was der Aufrufer bekommt. Es ist wenig, und das ist der Punkt.
Die Antwort enthält weder die SQL-Anweisung noch den Dateinamen, der Fehlerkanal beides. Genau so gehört es sich.
Nach außen gehört nicht: Stacktraces, Dateipfade, SQL-Anweisungen, Namen interner Funktionen, Versionsnummern. Wer wissen will, womit dein Server gebaut ist, muss sonst nur einen Fehler auslösen.
Nach innen gehört alles. Im Protokoll darf und soll der volle Fehler stehen, du liest es ja selbst.
Die Kennung verbindet beide Seiten. Sie steht in der Antwort und im Protokoll, und wenn dir jemand schreibt „ich bekomme einen Fehler a1b2c3”, findest du die Zeile in einer Sekunde. In einem echten Projekt wäre das eine Zufallszeichenkette je Anfrage; das Beispiel nimmt eine feste, damit man die beiden Seiten nebeneinanderlegen kann.
4xx oder 5xx
Zum Schluss die Frage, die dir die Entscheidung meistens abnimmt: Kann der Aufrufer daran etwas ändern?
Wenn ja, ist es ein 4xx, und dann darf er auch erfahren, was los ist. Ein fehlendes Feld, eine unbekannte Kennung, ein falsches Format: Sag es ihm, sonst kann er es nicht richtig machen.
Wenn nein, ist es ein 5xx, und dann geht es ihn nichts an. Die Datenbank ist weg, eine Datei fehlt, irgendwo steht ein Tippfehler in deinem Code. Er kann nichts tun außer es später noch einmal zu versuchen, und dafür reicht ein Satz.
Zum Mitnehmen
Eine Form für jeden Fehler, erzeugt an einer Stelle. Der Code ist für das Programm, die Meldung für den Menschen, und der Stacktrace bleibt im Protokoll.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.