81 lines
2.3 KiB
Python
81 lines
2.3 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import logging
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
class RocketChatError(Exception):
|
||
|
|
def __init__(self, status_code: int, message: str, api_error: dict | None = None) -> None:
|
||
|
|
self.status_code = status_code
|
||
|
|
self.message = message
|
||
|
|
self.api_error = api_error
|
||
|
|
super().__init__(f"Rocket.Chat API error {status_code}: {message}")
|
||
|
|
|
||
|
|
|
||
|
|
class RocketChatNetworkError(RocketChatError):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class RocketChatAuthError(RocketChatError):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class RocketChatRateLimitError(RocketChatError):
|
||
|
|
def __init__(self, status_code: int, message: str, retry_after_ms: float = 5000) -> None:
|
||
|
|
super().__init__(status_code, message)
|
||
|
|
self.retry_after_ms = retry_after_ms
|
||
|
|
|
||
|
|
|
||
|
|
def parse_rocketchat_error(response: httpx.Response) -> RocketChatError:
|
||
|
|
body = None
|
||
|
|
try:
|
||
|
|
body = response.json()
|
||
|
|
error_msg = body.get("error", body.get("message", response.text))
|
||
|
|
except Exception:
|
||
|
|
error_msg = response.text
|
||
|
|
|
||
|
|
status_code = response.status_code
|
||
|
|
|
||
|
|
if status_code in (401, 403):
|
||
|
|
return RocketChatAuthError(status_code, error_msg)
|
||
|
|
if status_code == 429:
|
||
|
|
retry_after_ms = _parse_retry_after(response) or 5000
|
||
|
|
return RocketChatRateLimitError(status_code, error_msg, retry_after_ms)
|
||
|
|
if status_code >= 500:
|
||
|
|
return RocketChatNetworkError(status_code, error_msg)
|
||
|
|
|
||
|
|
return RocketChatError(status_code, error_msg, body)
|
||
|
|
|
||
|
|
|
||
|
|
def _parse_retry_after(response: httpx.Response) -> float | None:
|
||
|
|
header = response.headers.get("Retry-After", "")
|
||
|
|
if not header:
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
return float(header) * 1000
|
||
|
|
except ValueError:
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def is_retryable_error(status_code: int) -> bool:
|
||
|
|
return status_code == 429 or status_code >= 500
|
||
|
|
|
||
|
|
|
||
|
|
def classify_error(
|
||
|
|
error: Exception,
|
||
|
|
) -> tuple[str, bool]:
|
||
|
|
if isinstance(error, RocketChatAuthError):
|
||
|
|
return "auth", False
|
||
|
|
if isinstance(error, RocketChatRateLimitError):
|
||
|
|
return "rate_limit", True
|
||
|
|
if isinstance(error, RocketChatNetworkError):
|
||
|
|
return "network", True
|
||
|
|
if isinstance(error, RocketChatError):
|
||
|
|
if error.status_code == 404:
|
||
|
|
return "not_found", False
|
||
|
|
return "api_error", is_retryable_error(error.status_code)
|
||
|
|
return "unknown", True
|