Files
Random-image-api/backend/app/core/errors.py
T
2026-02-22 18:58:55 +08:00

188 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Any
class ErrorCode(str, Enum):
# 4.1 通用
BAD_REQUEST = "BAD_REQUEST"
UNAUTHORIZED = "UNAUTHORIZED"
FORBIDDEN = "FORBIDDEN"
NOT_FOUND = "NOT_FOUND"
RATE_LIMITED = "RATE_LIMITED"
INTERNAL_ERROR = "INTERNAL_ERROR"
# 4.2 Random
NO_MATCH = "NO_MATCH"
UPSTREAM_STREAM_ERROR = "UPSTREAM_STREAM_ERROR"
UPSTREAM_403 = "UPSTREAM_403"
UPSTREAM_404 = "UPSTREAM_404"
UPSTREAM_RATE_LIMIT = "UPSTREAM_RATE_LIMIT"
# 4.3 Import
INVALID_UPLOAD_TYPE = "INVALID_UPLOAD_TYPE"
PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE"
UNSUPPORTED_URL = "UNSUPPORTED_URL"
# 4.4 Pixiv/Token
TOKEN_REFRESH_FAILED = "TOKEN_REFRESH_FAILED"
TOKEN_BACKOFF = "TOKEN_BACKOFF"
NO_TOKEN_AVAILABLE = "NO_TOKEN_AVAILABLE"
# 4.5 Proxy
PROXY_REQUIRED = "PROXY_REQUIRED"
PROXY_AUTH_FAILED = "PROXY_AUTH_FAILED"
PROXY_CONNECT_FAILED = "PROXY_CONNECT_FAILED"
UNKNOWN_REQUEST_ID = "req_unknown"
_DEFAULT_ZH_MESSAGE_BY_CODE: dict[ErrorCode, str] = {
ErrorCode.BAD_REQUEST: "请求参数错误",
ErrorCode.UNAUTHORIZED: "未授权",
ErrorCode.FORBIDDEN: "禁止访问",
ErrorCode.NOT_FOUND: "资源不存在",
ErrorCode.RATE_LIMITED: "请求过于频繁",
ErrorCode.INTERNAL_ERROR: "服务器内部错误",
ErrorCode.NO_MATCH: "没有匹配的图片",
ErrorCode.UPSTREAM_STREAM_ERROR: "上游请求失败",
ErrorCode.UPSTREAM_403: "上游拒绝访问(403)",
ErrorCode.UPSTREAM_404: "上游资源不存在(404)",
ErrorCode.UPSTREAM_RATE_LIMIT: "上游触发限流(429)",
ErrorCode.INVALID_UPLOAD_TYPE: "上传类型不支持",
ErrorCode.PAYLOAD_TOO_LARGE: "请求体过大",
ErrorCode.UNSUPPORTED_URL: "链接格式不支持",
ErrorCode.TOKEN_REFRESH_FAILED: "令牌刷新失败",
ErrorCode.TOKEN_BACKOFF: "令牌已进入退避",
ErrorCode.NO_TOKEN_AVAILABLE: "没有可用令牌",
ErrorCode.PROXY_REQUIRED: "需要代理",
ErrorCode.PROXY_AUTH_FAILED: "代理认证失败",
ErrorCode.PROXY_CONNECT_FAILED: "代理连接失败",
}
def _is_ascii_only(text: str) -> bool:
return all(ord(ch) < 128 for ch in text)
_ASCII_MESSAGE_MAP: dict[str, str] = {
"missing api key": "缺少 API Key",
"invalid api key": "API Key 无效",
"rate limited": "请求过于频繁",
"database unavailable": "数据库不可用",
"invalid json body": "JSON 请求体无效",
"unsupported limit": "limit 参数不支持",
"unsupported cursor": "cursor 参数不支持",
"unsupported status": "status 参数不支持",
"unsupported type": "type 参数不支持",
"unsupported format": "format 参数不支持",
"unsupported redirect": "redirect 参数不支持",
"unsupported seed": "seed 参数不支持",
"unsupported ai_type": "ai_type 参数不支持",
"unsupported orientation": "orientation 参数不支持",
"unsupported r18": "r18 参数不支持",
"unsupported min_*": "min_* 参数不支持",
"too many tag filters": "标签筛选条件过多",
"pixiv oauth not configured": "未配置 Pixiv OAuth(client_id/client_secret)",
"encryption not configured": "未配置加密密钥(FIELD_ENCRYPTION_KEY)",
"invalid stored token": "令牌密文无效(无法解密)",
"token not found": "令牌不存在",
"job not found": "任务不存在",
"proxy endpoint not found": "代理节点不存在",
"proxy pool not found": "代理池不存在",
"job is running": "任务正在运行中",
"proxy authentication failed": "代理认证失败",
"proxy connect failed": "代理连接失败",
"upstream request failed": "上游请求失败",
"upstream forbidden": "上游拒绝访问(403)",
"upstream not found": "上游资源不存在(404)",
"upstream rate limited": "上游触发限流(429)",
"upstream error": "上游错误",
"upstream request failed after attempts.": "多次尝试后上游请求仍失败",
"invalid proxy endpoint": "代理节点配置无效",
"unsupported conflict_policy": "conflict_policy 参数不支持",
}
def normalize_error_message(*, code: ErrorCode, message: str) -> str:
msg = str(message or "").strip()
if not msg:
return _DEFAULT_ZH_MESSAGE_BY_CODE.get(code, "请求失败")
if not _is_ascii_only(msg):
return msg
key = msg.lower()
if key in _ASCII_MESSAGE_MAP:
return _ASCII_MESSAGE_MAP[key]
if key.startswith("unsupported "):
rest = msg[len("Unsupported ") :].strip()
return f"参数不支持:{rest}" if rest else _DEFAULT_ZH_MESSAGE_BY_CODE.get(code, "请求失败")
if key.startswith("invalid "):
rest = msg[len("Invalid ") :].strip()
return f"参数无效:{rest}" if rest else _DEFAULT_ZH_MESSAGE_BY_CODE.get(code, "请求失败")
if key.startswith("missing "):
rest = msg[len("Missing ") :].strip()
return f"缺少字段:{rest}" if rest else _DEFAULT_ZH_MESSAGE_BY_CODE.get(code, "请求失败")
return _DEFAULT_ZH_MESSAGE_BY_CODE.get(code, "请求失败")
@dataclass(frozen=True, slots=True)
class ApiError(Exception):
code: ErrorCode
message: str
status_code: int = 400
details: dict[str, Any] | None = None
def _coerce_request_id(request_id: str | None) -> str:
request_id = (request_id or "").strip()
return request_id if request_id else UNKNOWN_REQUEST_ID
def _request_id_from_request(request: Any | None) -> str | None:
if request is None:
return None
request_id = getattr(getattr(request, "state", None), "request_id", None)
if request_id:
return str(request_id)
header = request.headers.get("X-Request-Id")
return header.strip() if header else None
def error_body(
*,
code: ErrorCode,
message: str,
request_id: str | None = None,
details: dict[str, Any] | None = None,
) -> dict[str, Any]:
return {
"ok": False,
"code": code.value,
"message": normalize_error_message(code=code, message=message),
"request_id": _coerce_request_id(request_id),
"details": details or {},
}
def json_error_response(
*,
code: ErrorCode,
message: str,
status_code: int,
request: Any | None = None,
request_id: str | None = None,
details: dict[str, Any] | None = None,
) -> Any:
if request_id is None:
request_id = _request_id_from_request(request)
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=status_code,
content=error_body(code=code, message=message, request_id=request_id, details=details),
)