Fehlerantworten 400/409 mit ProblemDetails zentral ins Schema (ADR 0037) #129

Open
opened 2026-10-22 16:02:50 +00:00 by claude · 1 comment
Collaborator

Entschieden am 22.10.2026, siehe docs/adr/0037-fehlerantworten-zentral-im-schema.md.

Ziel

Das Fehlerformat steht im Vertrag. Zur Laufzeit gibt es das schon: ProblemDetailsMapper macht aus jeder WebApplicationException ein ProblemDetails (RFC 9457) mit application/problem+json. Im Schema fehlt es, weil kein Endpunkt ProblemDetails als Rückgabetyp nennt.

Umfang

  1. Filter: Ein OASFilter (in shared) hängt an jede POST-, PUT- und DELETE-Operation die Antworten 400 und 409 mit $ref: ProblemDetails und Content-Type application/problem+json an. Er wird über mp.openapi.filter eingetragen.
  2. Schema: ProblemDetails muss als Komponente in openapi.json stehen. Der Filter legt sie an, oder sie wird als Schema registriert. Danach mvn -B clean verify laufen lassen und openapi.json mitcommitten.
  3. Frontend: packages/shared/src/api.ts liest den Fehlerkörper über den generierten Typ ProblemDetails. Die Tests in packages/shared werden angepasst.
  4. Vorher aufzählen: Kann ein GET einen 409 oder einen 400 mit Fachtext liefern? Ein Kandidat ist SeasonResolver. Wenn ja, bezieht der Filter GET mit ein. Das wird vor dem Bau entschieden und nicht geraten.
  5. ArchUnit: Gehört der Filter nach shared/api oder in ein eigenes Paket? Die Tabelle in ADR 0033 entscheidet. Braucht er ein neues Paket, fragen.

Nicht drin

  • Die Bedingungen je Endpunkt beschreiben. Das ist bewusst offen (ADR 0037) und wird erst mit eigenen Fehlercodes neu geprüft.
  • 401 und 403 schreibt SmallRye schon aus @RolesAllowed ins Schema. Ob ihr Körper auch ProblemDetails ist, wird im Bau nachgesehen und im Issue festgehalten.

Abnahme

  • mvn -B clean verify, die Frontend-Builds, E2E und metrics.py diff sind grün.
  • In openapi.json hat jede schreibende Operation 400 und 409 mit ProblemDetails.
  • Gegenprobe: Ein Backend-Test löst einen echten 409 aus (zum Beispiel eine zweite offene Saison anlegen) und prüft Status, Content-Type application/problem+json und detail.
Entschieden am 22.10.2026, siehe `docs/adr/0037-fehlerantworten-zentral-im-schema.md`. ## Ziel Das Fehlerformat steht im Vertrag. Zur Laufzeit gibt es das schon: `ProblemDetailsMapper` macht aus jeder `WebApplicationException` ein `ProblemDetails` (RFC 9457) mit `application/problem+json`. Im Schema fehlt es, weil kein Endpunkt `ProblemDetails` als Rückgabetyp nennt. ## Umfang 1. **Filter:** Ein `OASFilter` (in `shared`) hängt an jede POST-, PUT- und DELETE-Operation die Antworten `400` und `409` mit `$ref: ProblemDetails` und Content-Type `application/problem+json` an. Er wird über `mp.openapi.filter` eingetragen. 2. **Schema:** `ProblemDetails` muss als Komponente in `openapi.json` stehen. Der Filter legt sie an, oder sie wird als Schema registriert. Danach `mvn -B clean verify` laufen lassen und `openapi.json` mitcommitten. 3. **Frontend:** `packages/shared/src/api.ts` liest den Fehlerkörper über den generierten Typ `ProblemDetails`. Die Tests in `packages/shared` werden angepasst. 4. **Vorher aufzählen:** Kann ein GET einen 409 oder einen 400 mit Fachtext liefern? Ein Kandidat ist `SeasonResolver`. Wenn ja, bezieht der Filter GET mit ein. Das wird vor dem Bau entschieden und nicht geraten. 5. **ArchUnit:** Gehört der Filter nach `shared/api` oder in ein eigenes Paket? Die Tabelle in ADR 0033 entscheidet. Braucht er ein neues Paket, fragen. ## Nicht drin - Die Bedingungen je Endpunkt beschreiben. Das ist bewusst offen (ADR 0037) und wird erst mit eigenen Fehlercodes neu geprüft. - 401 und 403 schreibt SmallRye schon aus `@RolesAllowed` ins Schema. Ob ihr Körper auch `ProblemDetails` ist, wird im Bau nachgesehen und im Issue festgehalten. ## Abnahme - `mvn -B clean verify`, die Frontend-Builds, E2E und `metrics.py diff` sind grün. - In `openapi.json` hat jede schreibende Operation `400` und `409` mit `ProblemDetails`. - **Gegenprobe:** Ein Backend-Test löst einen echten 409 aus (zum Beispiel eine zweite offene Saison anlegen) und prüft Status, Content-Type `application/problem+json` und `detail`.
Author
Collaborator

Zusatz 22.10.2026: GET /api/seasons/active liefert 404 statt 204

Entschieden beim Zalando-Review von /api/seasons. Es gehört hierher, weil api.ts dafür denselben Umbau braucht.

Was sich ändert: Ist keine Saison offen, antwortet GET /api/seasons/active mit 404 und ProblemDetails, detail = „Es ist keine Saison offen.“ Heute kommt ein 204 ohne Body, der nicht im Schema steht. Zalando verlangt für einen GET auf eine einzelne Ressource, die es nicht gibt, 404. Den Fall gibt es beim Saisonwechsel: Die alte Saison ist abgeschlossen, die neue noch nicht angelegt.

api.ts: Der geworfene Fehler trägt den HTTP-Status, und, sobald es den Typ gibt, das ProblemDetails. Heute ist es nur ein Error mit Text.

Muss im selben Umbau angepasst werden, sonst geht es kaputt:

  • Dashboard (portal/src/pages/verwaltung/DashboardPage.tsx, loadData): 404 heisst „keine Saison“ und zeigt das Formular „Neue Saison eröffnen“. Ohne Anpassung erscheint ein Fehler-Toast statt des Formulars, und keine neue Saison lässt sich anlegen.
  • Vereinsmeldung (portal/src/hooks/useClubRegistration.ts): 404 heisst „Meldung geschlossen“ (CLOSED). Ohne Anpassung bleibt phase auf null, und die Seite sieht bearbeitbar aus.

Ohne Änderung weiter richtig, weil sie jeden Fehler als „keine Saison“ behandeln: Spielbetrieb, Spielplan, Veröffentlichen, Kategorien zuteilen, Cup-Tableau, Elo-Bestimmung, Turnierliste.

Besser ohne eigenes Zutun: Auf-/Abstieg, Spielwochen und Teilnehmer zeigen statt „Cannot read properties of undefined (reading 'id')“ den Text „Es ist keine Saison offen.“

Skripte: matchblatt-import.py und import-ttvp-termine.py fragen /active ab. Ohne Saison brechen sie weiter ab. Prüfen, dass die Meldung dabei lesbar ist.

Tests (mit Ollis Ja vom 22.10.):

  • SeasonActiveEmptyTest: Erwartung 204 → 404 mit detail.
  • portal/src/test/DashboardPage.test.tsx: „keine Saison“ als 404 simulieren statt als leere Antwort.
  • E2E workflow-archive schliesst eine Saison ab und legt eine neue über das Dashboard an. Er deckt den gefährlichen Fall ab und muss grün bleiben.

GET /api/seasons/active/name liefert ebenfalls 204, hat aber keinen Aufrufer und soll wegfallen. Er wird nicht umgestellt, sondern gelöscht. Ob das hier geschieht oder beim Aufräumen der Season-Endpunkte, entscheidet der Bau.

Abnahme zusätzlich: Lokal oder auf Staging eine Saison abschliessen. Das Dashboard zeigt „Neue Saison eröffnen“ ohne Fehler-Toast. Die Vereinsmeldung ist gesperrt. Auf-/Abstieg zeigt „Es ist keine Saison offen.“

## Zusatz 22.10.2026: `GET /api/seasons/active` liefert 404 statt 204 Entschieden beim Zalando-Review von `/api/seasons`. Es gehört hierher, weil `api.ts` dafür denselben Umbau braucht. **Was sich ändert:** Ist keine Saison offen, antwortet `GET /api/seasons/active` mit `404` und `ProblemDetails`, `detail` = „Es ist keine Saison offen.“ Heute kommt ein `204` ohne Body, der nicht im Schema steht. Zalando verlangt für einen GET auf eine einzelne Ressource, die es nicht gibt, 404. Den Fall gibt es beim Saisonwechsel: Die alte Saison ist abgeschlossen, die neue noch nicht angelegt. **`api.ts`:** Der geworfene Fehler trägt den HTTP-Status, und, sobald es den Typ gibt, das `ProblemDetails`. Heute ist es nur ein `Error` mit Text. **Muss im selben Umbau angepasst werden, sonst geht es kaputt:** - **Dashboard** (`portal/src/pages/verwaltung/DashboardPage.tsx`, `loadData`): 404 heisst „keine Saison“ und zeigt das Formular „Neue Saison eröffnen“. Ohne Anpassung erscheint ein Fehler-Toast statt des Formulars, und **keine neue Saison lässt sich anlegen**. - **Vereinsmeldung** (`portal/src/hooks/useClubRegistration.ts`): 404 heisst „Meldung geschlossen“ (`CLOSED`). Ohne Anpassung bleibt `phase` auf `null`, und die Seite sieht bearbeitbar aus. **Ohne Änderung weiter richtig**, weil sie jeden Fehler als „keine Saison“ behandeln: Spielbetrieb, Spielplan, Veröffentlichen, Kategorien zuteilen, Cup-Tableau, Elo-Bestimmung, Turnierliste. **Besser ohne eigenes Zutun:** Auf-/Abstieg, Spielwochen und Teilnehmer zeigen statt „Cannot read properties of undefined (reading 'id')“ den Text „Es ist keine Saison offen.“ **Skripte:** `matchblatt-import.py` und `import-ttvp-termine.py` fragen `/active` ab. Ohne Saison brechen sie weiter ab. Prüfen, dass die Meldung dabei lesbar ist. **Tests (mit Ollis Ja vom 22.10.):** - `SeasonActiveEmptyTest`: Erwartung `204` → `404` mit `detail`. - `portal/src/test/DashboardPage.test.tsx`: „keine Saison“ als 404 simulieren statt als leere Antwort. - E2E `workflow-archive` schliesst eine Saison ab und legt eine neue über das Dashboard an. Er deckt den gefährlichen Fall ab und muss grün bleiben. **`GET /api/seasons/active/name`** liefert ebenfalls 204, hat aber keinen Aufrufer und soll wegfallen. Er wird nicht umgestellt, sondern gelöscht. Ob das hier geschieht oder beim Aufräumen der Season-Endpunkte, entscheidet der Bau. **Abnahme zusätzlich:** Lokal oder auf Staging eine Saison abschliessen. Das Dashboard zeigt „Neue Saison eröffnen“ ohne Fehler-Toast. Die Vereinsmeldung ist gesperrt. Auf-/Abstieg zeigt „Es ist keine Saison offen.“
Sign in to join this conversation.
No description provided.