Files
davideandClaude Opus 5 0cf35147ad Answer user-facing API failures with a machine-readable error code
The dashboard now speaks seven languages but every failure path still showed
the API's raw English text ("insufficient balance", "current password is
incorrect"), which is the most frequent and least forgiving part of the UI to
leave untranslated.

Rather than teach the API about locales, it keeps answering in one language
and hands the client something to translate: `detail` becomes
{code, message, params}, where message stays English for non-dashboard
consumers (curl, tests) and code maps onto `error.<code>` in i18n.js. An
unknown code falls back to message, so a client older or newer than the server
degrades to English instead of a blank toast.

Domain exceptions (BetError, WithdrawalError) subclass the new ApiError and
carry the code from where the failure actually happens; str(exc) is still the
English message, so existing tests keep matching on it. Interpolated values
travel in params rather than baked into the English sentence — amounts as
*_sats, from which the frontend derives a *_plm sibling, so each language can
place them wherever its grammar wants.

admin.js reads detail.message defensively: the admin endpoints still return a
bare string, but the shared auth dependencies now return the structured form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:44:39 +02:00

48 lines
1.9 KiB
Python

"""Machine-readable error codes for user-facing API failures.
The dashboard is multilingual (app/static/i18n.js) but the API is not: every
message produced here stays English. What travels alongside it is a stable
`code` the client maps onto its own translated string (`error.<code>`), falling
back to `message` for any code it doesn't recognize — so a non-dashboard
consumer (curl, tests, a future client) still gets something readable without
having to know the code table.
`detail` is therefore an object rather than the FastAPI-default bare string:
{"code": "insufficient_balance", "message": "insufficient balance", "params": {}}
`params` carries the values interpolated into the message (amounts, limits) so
the translated string can place them wherever its own grammar needs them,
instead of the client having to parse them back out of the English text.
"""
from typing import Any
from fastapi import HTTPException
class ApiError(Exception):
"""Domain-layer error carrying the code the client will translate.
Subclassed per domain (BetError, WithdrawalError) so services keep raising
their own exception type. `str(exc)` is still the plain English message.
"""
def __init__(self, code: str, message: str, **params: Any) -> None:
super().__init__(message)
self.code = code
self.message = message
self.params = params
def as_detail(self) -> dict[str, Any]:
return {"code": self.code, "message": self.message, "params": self.params}
def http_error(status_code: int, code: str, message: str, **params: Any) -> HTTPException:
"""HTTPException whose detail is the structured object described above."""
return HTTPException(status_code, ApiError(code, message, **params).as_detail())
def from_api_error(status_code: int, exc: ApiError) -> HTTPException:
return HTTPException(status_code, exc.as_detail())