xtc-backend / app /errors.py
a3216's picture
deploy: initial release to Hugging Face Spaces
36fae79
Raw
History Blame Contribute Delete
5.41 kB
"""统一错误模型。
完全兼容原 Netlify 版返回格式:
{ "ok": false, "error": { "code", "message", "status", "retryable", "hint", "upstream?", "details?" } }
错误码集合:bad_request/unauthorized/forbidden/not_found/request_timeout/
quota_exceeded/internal_error/service_unavailable/upstream_timeout/
model_disabled/server_misconfigured
"""
from __future__ import annotations
from typing import Any, Optional
from fastapi import Request
from fastapi.responses import JSONResponse
# 状态码 -> 错误码
_STATUS_TO_CODE = {
400: "bad_request",
401: "unauthorized",
402: "payment_required",
403: "forbidden",
404: "not_found",
405: "method_not_allowed",
408: "request_timeout",
409: "conflict",
422: "unprocessable_entity",
429: "quota_exceeded",
500: "internal_error",
502: "bad_gateway",
503: "service_unavailable",
504: "upstream_timeout",
}
# 可重试状态码
_RETRYABLE_STATUS = {408, 409, 429, 500, 502, 503, 504}
class HttpError(Exception):
"""统一 HTTP 异常。"""
def __init__(
self,
message: str,
status: int = 500,
code: Optional[str] = None,
details: Any = None,
upstream: Any = None,
) -> None:
super().__init__(message)
self.message = message
self.status = status
self.code = code or _STATUS_TO_CODE.get(status, "internal_error")
self.details = details
self.upstream = upstream
def error_code_for_status(status: int, fallback: str = "upstream_error") -> str:
return _STATUS_TO_CODE.get(status, fallback)
def is_retryable_status(status: int) -> bool:
return status in _RETRYABLE_STATUS
def get_error_hint(*, code: str, status: int, message: str) -> Optional[str]:
"""生成给前端展示的提示文案。"""
text = (message or "").lower()
if code == "unauthorized" or status == 401:
return "请检查 access_key/access_token 是否正确,或先重新生成临时令牌"
if code == "forbidden" or status == 403:
return "当前账号或策略不允许此操作,请检查后台厂商与模型策略"
if code == "not_found" or status == 404:
return "资源不存在,请检查接口路径、provider 与 model 参数"
if code == "quota_exceeded" or status == 429:
return "请求过快或额度不足,建议稍后重试或切换厂商/模型"
if code == "upstream_timeout" or status == 504 or "timeout" in text:
return "上游超时,建议降低 max_tokens 或稍后重试"
if code == "bad_request" or status in (400, 422):
return "请求参数格式可能有误,请检查 messages/images/model 字段"
if status >= 500:
return "服务暂时不可用,请稍后重试"
return None
def make_error_response(
*,
message: str,
status: int = 500,
code: Optional[str] = None,
details: Any = None,
upstream: Any = None,
) -> JSONResponse:
err_code = code or _STATUS_TO_CODE.get(status, "internal_error")
hint = get_error_hint(code=err_code, status=status, message=message)
payload = {
"ok": False,
"error": {
"code": err_code,
"message": message,
"status": status,
"retryable": is_retryable_status(status),
"hint": hint,
},
}
if details is not None:
payload["error"]["details"] = details
if upstream is not None:
payload["error"]["upstream"] = upstream
return JSONResponse(status_code=status, content=payload, headers=_CORS_HEADERS)
def make_upstream_error_response(
*,
status: int,
text: str,
json_body: Any = None,
fallback_code: str = "upstream_error",
) -> JSONResponse:
"""从上游响应构造统一错误。"""
upstream_err = None
if isinstance(json_body, dict):
upstream_err = json_body.get("error") if isinstance(json_body.get("error"), dict) else json_body
code = (upstream_err or {}).get("code") if isinstance(upstream_err, dict) else None
code = code or error_code_for_status(status, fallback_code)
message = (upstream_err or {}).get("message") if isinstance(upstream_err, dict) else None
message = message or text or f"Upstream error ({status})"
hint = get_error_hint(code=code, status=status, message=message)
payload = {
"ok": False,
"error": {
"code": code,
"message": message,
"status": status,
"retryable": is_retryable_status(status),
"hint": hint,
"upstream": upstream_err,
},
}
return JSONResponse(status_code=status, content=payload, headers=_CORS_HEADERS)
_CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*",
}
async def http_error_handler(request: Request, exc: HttpError) -> JSONResponse:
return make_error_response(
message=exc.message,
status=exc.status,
code=exc.code,
details=exc.details,
upstream=exc.upstream,
)
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
import traceback
traceback.print_exc()
return make_error_response(
message=str(exc) or "Unexpected error",
status=500,
code="internal_error",
)