"""Типизированные исключения клиента.
Каждый класс = одна ситуация, в которой сервису нужно разное поведение:
401 — ключ потерян/отозван (пересоздать), 403 — source в конверте не совпал
с источником ключа (анти-спуфинг, ошибка кода), 422 — конверт не принят
(ошибка кода или тип не зарегистрирован), 5xx — Synapse лежит, connection —
недоступен. `detail` — человекочитаемое сообщение, `status_code` — HTTP-код,
если был ответ.
"""
from __future__ import annotations
class SynapseError(RuntimeError):
"""База: всё, что клиент бросает, ловится этим классом."""
def __init__(self, detail: str, *, status_code: int | None = None) -> None:
super().__init__(detail)
self.detail = detail
self.status_code = status_code
class SynapseConfigError(SynapseError):
"""Нет url/api_key в аргументах и env, или конфиг в принципе битый."""
class SynapseConnectionError(SynapseError):
"""HTTP-транспорт недоступен (DNS, refusal, timeout). __cause__ — оригинал."""
class SynapseAuthError(SynapseError):
"""401: ключ отсутствует, неизвестен или отозван (источник архива — тоже сюда)."""
class SynapseForbiddenError(SynapseError):
"""403: поле source в конверте не совпадает с источником ключа."""
class SynapseNotFoundError(SynapseError):
"""404: событие с таким id у источника ключа не найдено."""
class SynapseValidationError(SynapseError):
"""422 от сервера ИЛИ локальная ошибка конверта (status_code=None)."""
class SynapseServerError(SynapseError):
"""5xx: Synapse недоступен/ошибка на его стороне — сообщение не ушло."""
class SynapseWebhookError(SynapseError):
"""s2s-доставка не прошла проверку: нет/битый заголовок подписи,
чужой секрет, replay (тело старше окна свежести), не-JSON тело.
Локальная ошибка приёма (status_code=None) — это не про отправку."""