fix: Fehler am Kurzbefehl-Endpunkt sind auf dem Telefon lesbar

Der Kurzbefehl zeigt das Feld "message" an. Fehlerantworten liefern aber
"detail" – die Mitteilung auf dem Telefon blieb also leer. Man sah nicht
einmal, DASS etwas schiefging: Die Marke wurde als benutzt vermerkt, auf dem
Server kam trotzdem nichts an, und die Ursache war von aussen unsichtbar.

Antworten unter /api/quick liefern jetzt zusaetzlich "message" mit einem
lesbaren Text. Bei fehlenden Pflichtfeldern nennt er sie beim Namen und
erinnert daran, dass der Anfragetext beim Datei-Kurzbefehl 'Formular' sein
muss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-22 23:47:10 +02:00
co-authored by Claude Opus 5
parent 84605cbc0f
commit 5ff9b63d0a
2 changed files with 72 additions and 1 deletions
+40 -1
View File
@@ -10,7 +10,11 @@ import threading
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, Depends, FastAPI, HTTPException from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -155,6 +159,41 @@ api.include_router(quick.router)
api.include_router(inbox.router) api.include_router(inbox.router)
api.include_router(mail.router) api.include_router(mail.router)
# Fehler an den Kurzbefehl-Endpunkten zusätzlich als „message" ausliefern.
# Der Kurzbefehl auf dem Telefon zeigt genau dieses Feld an; bei einem Fehler
# steht sonst nur „detail" in der Antwort und die Mitteilung bleibt leer –
# man sieht dann nicht, DASS etwas schiefging, geschweige denn was.
def _ist_kurzbefehl(request: Request) -> bool:
return request.url.path.startswith("/api/quick/")
@app.exception_handler(StarletteHTTPException)
async def http_fehler(request: Request, exc: StarletteHTTPException):
inhalt = {"detail": exc.detail}
if _ist_kurzbefehl(request):
inhalt["message"] = str(exc.detail)
inhalt["ok"] = False
return JSONResponse(status_code=exc.status_code, content=inhalt,
headers=getattr(exc, "headers", None))
@app.exception_handler(RequestValidationError)
async def eingabe_fehler(request: Request, exc: RequestValidationError):
inhalt = {"detail": exc.errors()}
if _ist_kurzbefehl(request):
fehlend = [
".".join(str(t) for t in f.get("loc", ()) if t not in ("body", "query"))
for f in exc.errors()
]
inhalt["message"] = ("Die Anfrage ist unvollständig: "
+ ", ".join(f for f in fehlend if f)
+ ". Beim Datei-Kurzbefehl muss der Anfragetext "
+ "'Formular' sein, mit den Feldern file (Datei) "
+ "und account_id.")
inhalt["ok"] = False
return JSONResponse(status_code=422, content=jsonable_encoder(inhalt))
app.include_router(api) app.include_router(api)
+32
View File
@@ -307,3 +307,35 @@ def test_leere_und_zu_grosse_dateien_werden_abgewiesen(admin):
files={"file": ("gross.csv", io.BytesIO(b"x" * (6 * 1024 * 1024)), "text/csv")}, files={"file": ("gross.csv", io.BytesIO(b"x" * (6 * 1024 * 1024)), "text/csv")},
data={"account_id": str(konto["id"])}, headers=kopf) data={"account_id": str(konto["id"])}, headers=kopf)
assert riesig.status_code == 413 assert riesig.status_code == 413
def test_fehler_am_kurzbefehl_sind_im_feld_message_lesbar(admin):
"""Der Kurzbefehl zeigt „message" an. Steht dort bei einem Fehler nichts,
sieht man auf dem Telefon nur eine leere Mitteilung – und sucht den Fehler
dann auf dem Server statt im Kurzbefehl."""
client = _fremd()
konto = _konto(admin, name="Meldungskonto")
marke = _marke(admin, konto["id"])
kopf = {"X-Certain-Token": marke["token"]}
import io
# Datei da, aber account_id vergessen: Pflichtfeld fehlt.
r = client.post("/api/quick/statement",
files={"file": ("a.csv", io.BytesIO(b"Datum;Betrag\n"), "text/csv")},
headers=kopf)
assert r.status_code == 422
assert "account_id" in r.json()["message"]
assert "Formular" in r.json()["message"]
# Falsche Marke.
r = client.post("/api/quick/booking", json={"amount": "1"},
headers={"X-Certain-Token": "falsch"})
assert r.status_code == 401
assert "message" in r.json()
# Leere Datei.
r = client.post("/api/quick/statement",
files={"file": ("leer.csv", io.BytesIO(b""), "text/csv")},
data={"account_id": str(konto["id"])}, headers=kopf)
assert r.status_code == 400
assert "leer" in r.json()["message"].lower()