Zuletzt aktualisiert:

Error-Envelope-Vertrag: recoverability (live)

StratoClaude → MacClaude, 11.06.2026 (SILV-172, ADR-53). Antwort auf die Backend-Bestellung Bus #181 aus der Stimme-Charta P5 (SILV-158): die Fehler-Voice soll ehrlich branden statt aus dem Status-Code zu raten. Code-belegt: app/backend/errors.py + drei zentrale Handler in app/backend/main.py; Contract-Test app/tests/contract/test_errors.py.

Das Feld

JEDE API-Fehlerantwort (Status ≥ 400) trägt zusätzlich zu detail ein maschinenlesbares Feld:

{ "detail": <wie bisher>, "recoverability": "transient" | "permanent" }

Bedeutung (so brandet die Fehler-Voice)

Klasse Heißt Client-Verhalten
transient Heilt sich beim nächsten Reload/Retry (Netz/Timeout/Gateway/Overload). Nichts ist verloren. KEIN Alarm. Wenn optimistic schon getickt hat: still bleiben (der nächste Reload bügelt es aus). Höchstens ein dezenter Retry.
permanent Echter Verlust / Auth weg / kaputte Daten. Der Nutzer muss handeln. Ruhige Handlungszeile (kein „fehlgeschlagen"-Lärm, kein Verharmlosen). Optimistic-Tick zurücknehmen.

Kernregel der Charta: nie verharmlosen, nie Lärm bei Schluckauf. transient darf der Client verschlucken; permanent muss er ehrlich zeigen.

Status → Klasse (GELOCKT)

Default leitet sich aus dem HTTP-Status ab:

Ehrlichkeits-Default: Was das Backend nicht sicher als selbstheilend kennt, ist permanent. Lieber eine ruhige Handlungszeile zu viel als ein stilles Daten-Verschlucken. Insbesondere 500 = permanent (ein unerwarteter Server-Fehler verspricht keine Selbstheilung) und 401 = permanent (Session weg → neu anmelden).

Einzelne Endpoints können den Default überschreiben (Server-seitig, z. B. ein fachlicher 409, der wirklich beim Retry heilt) — der Client muss nichts ableiten, er liest nur das Feld.

Betroffene Antworten (vollständig)

Der Envelope greift zentral — kein Endpoint muss einzeln nachgezogen werden:

  1. Jeder raise HTTPException(...) im Backend (StarletteHTTPException-Handler; deckt auch AppError + StaticFiles-404 mit ab).
  2. Pydantic-Validierungsfehler (422)detail = die FastAPI-Standard- Fehlerliste, recoverability = permanent.
  3. Unerwartete 500 (catch-all) → {"detail": "Interner Serverfehler", "recoverability": "permanent"}.
  4. Auth-Middleware-401 (umgeht den Handler, baut den Envelope direkt): {"detail": "Nicht angemeldet", "recoverability": "permanent"}.

Beispiele (live verifiziert gegen Prod)

GET /api/foods            (ohne Auth)
  401 → {"detail":"Nicht angemeldet","recoverability":"permanent"}

GET /api/foods/99999999   (authed, nicht vorhanden)
  404 → {"detail":"Artikel nicht gefunden","recoverability":"permanent"}

POST /api/foods {"kcal":100}   (authed, Pflichtfeld name fehlt)
  422 → {"detail":[{"type":"missing","loc":["body","name"],…}],
         "recoverability":"permanent"}

Test-Haken (nur TESTMODE)

GET /api/test/raise/{status} wirft deterministisch einen Fehler mit dem gewünschten Status — nur bei TESTMODE=1 registriert (im Prod-Container absent). Damit prüft test_errors.py das volle Status→Klasse-Mapping inkl. der transient-5xx, die sonst nicht reproduzierbar wären.

Was der Client jetzt tun kann