Download apps/core/exceptions/handlers.py from jun2024/fc: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/core/exceptions/handlers.py
- Command line
-
hf download hf://spaces/jun2024/fc/apps/core/exceptions/handlers.py
-
curl -L -o handlers.py https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/core/exceptions/handlers.py
16.1 kB
| """ | |
| 全局异常处理器 | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from collections.abc import Callable | |
| from typing import Any | |
| from django.core.exceptions import ObjectDoesNotExist | |
| from django.core.exceptions import PermissionDenied as DjangoPermissionDenied | |
| from django.http import Http404, HttpRequest, HttpResponse | |
| from ninja import NinjaAPI | |
| from ninja.errors import HttpError | |
| from ninja.errors import ValidationError as NinjaValidationError | |
| from .base import BusinessError, BusinessException | |
| from .common import ( | |
| AuthenticationError, | |
| ConflictError, | |
| NotFoundError, | |
| PermissionDenied, | |
| RateLimitError, | |
| ValidationException, | |
| ) | |
| from .external import ExternalServiceError, RecognitionTimeoutError, ServiceUnavailableError | |
| logger = logging.getLogger("api") | |
| __all__: list[str] = ["register_exception_handlers"] | |
| # Type alias for the create_response callable used across handler registrations | |
| _CreateResponse = Callable[..., HttpResponse] | |
| # --------------------------------------------------------------------------- | |
| # 辅助函数 | |
| # --------------------------------------------------------------------------- | |
| def _get_user_id(request: HttpRequest) -> int | str | None: | |
| user = getattr(request, "user", None) | |
| user_id: Any = getattr(user, "id", None) if user is not None else None | |
| if isinstance(user_id, (int, str)): | |
| return user_id | |
| auth = getattr(request, "auth", None) | |
| auth_id: Any = getattr(auth, "id", None) if auth is not None else None | |
| if isinstance(auth_id, (int, str)): | |
| return auth_id | |
| return None | |
| def _safe_log_value(value: Any, *, depth: int = 0) -> Any: | |
| if depth >= 4: | |
| return str(value)[:200] | |
| if value is None: | |
| return None | |
| if isinstance(value, (str, int, float, bool)): | |
| if isinstance(value, str) and len(value) > 200: | |
| return value[:200] + "..." | |
| return value | |
| if isinstance(value, dict): | |
| safe: dict[str, Any] = {} | |
| for k, v in list(value.items())[:50]: | |
| safe[str(k)[:100]] = _safe_log_value(v, depth=depth + 1) | |
| return safe | |
| if isinstance(value, list): | |
| return [_safe_log_value(v, depth=depth + 1) for v in value[:50]] | |
| if isinstance(value, tuple): | |
| return tuple(_safe_log_value(v, depth=depth + 1) for v in value[:50]) | |
| return str(value)[:200] | |
| def _log_extra(request: HttpRequest, **extra: Any) -> dict[str, Any]: | |
| base: dict[str, Any] = { | |
| "path": getattr(request, "path", None), | |
| "method": getattr(request, "method", None), | |
| "user_id": _get_user_id(request), | |
| } | |
| if "errors" in extra: | |
| extra["errors"] = _safe_log_value(extra.get("errors")) | |
| base.update(extra) | |
| return base | |
| def _attach_request_meta(request: HttpRequest, payload: Any) -> Any: | |
| """Attach request metadata (request_id, trace_id) to response payload.""" | |
| if not isinstance(payload, dict): | |
| return payload | |
| request_id = getattr(request, "request_id", None) or getattr(request, "headers", {}).get("X-Request-ID") | |
| try: | |
| from apps.core.infrastructure.request_context import get_trace_ids | |
| trace_id, span_id = get_trace_ids() | |
| except (ImportError, AttributeError): | |
| trace_id, span_id = None, None | |
| payload.setdefault("request_id", request_id) | |
| payload.setdefault("trace_id", trace_id or request_id) | |
| if span_id: | |
| payload.setdefault("span_id", span_id) | |
| return payload | |
| def _parse_retry_after(raw: Any) -> int | None: | |
| """Parse retry_after value from RateLimitError errors dict.""" | |
| if isinstance(raw, int): | |
| return raw | |
| if raw is not None: | |
| try: | |
| return int(raw) | |
| except (TypeError, ValueError): | |
| return None | |
| return None | |
| def _set_retry_after_header(response: HttpResponse, retry_after: int) -> None: | |
| """Set Retry-After response header.""" | |
| try: | |
| response.headers["Retry-After"] = str(max(0, retry_after)) | |
| except (AttributeError, TypeError, KeyError): | |
| response["Retry-After"] = str(max(0, retry_after)) | |
| def _resolve_llm_status_code(upstream: int | None) -> int: | |
| """Resolve HTTP status code for LLM API errors based on upstream status.""" | |
| upstream_map = {429: 429, 503: 503, 504: 504} | |
| return upstream_map.get(upstream, 502) if upstream else 502 | |
| # --------------------------------------------------------------------------- | |
| # 注册函数 | |
| # --------------------------------------------------------------------------- | |
| def register_exception_handlers(api: NinjaAPI) -> None: | |
| """注册全局异常处理器""" | |
| def _create_response(request: HttpRequest, payload: Any, *, status: int) -> HttpResponse: | |
| payload = _attach_request_meta(request, payload) | |
| return api.create_response(request, payload, status=status) # type: ignore[no-any-return] | |
| _register_business_handlers(api, _create_response) | |
| _register_llm_handlers(api, _create_response) | |
| _register_django_handlers(api, _create_response) | |
| _register_jwt_handler(api, _create_response) | |
| _register_fallback_handler(api, _create_response) | |
| def _register_business_handlers(api: NinjaAPI, create_response: _CreateResponse) -> None: | |
| """注册业务异常处理器""" | |
| _register_client_error_handlers(api, create_response) | |
| _register_server_error_handlers(api, create_response) | |
| def _register_client_error_handlers(api: NinjaAPI, create_response: _CreateResponse) -> None: | |
| """注册 4xx 业务异常处理器""" | |
| def handle_validation_exception(request: HttpRequest, exc: ValidationException) -> HttpResponse: | |
| logger.info("Validation failed: %s", exc.message, extra=_log_extra(request, code=exc.code, errors=exc.errors)) | |
| return create_response(request, exc.to_dict(), status=400) | |
| def handle_authentication_error(request: HttpRequest, exc: AuthenticationError) -> HttpResponse: | |
| logger.warning("Authentication failed: %s", exc.message, extra=_log_extra(request, code=exc.code)) | |
| return create_response(request, exc.to_dict(), status=401) | |
| def handle_permission_denied_exception(request: HttpRequest, exc: PermissionDenied) -> HttpResponse: | |
| logger.warning("Permission denied: %s", exc.message, extra=_log_extra(request, code=exc.code)) | |
| return create_response(request, exc.to_dict(), status=403) | |
| def handle_not_found_exception(request: HttpRequest, exc: NotFoundError) -> HttpResponse: | |
| logger.info("Not found: %s", exc.message, extra=_log_extra(request, code=exc.code)) | |
| return create_response(request, exc.to_dict(), status=404) | |
| def handle_conflict_exception(request: HttpRequest, exc: ConflictError) -> HttpResponse: | |
| logger.info("Conflict: %s", exc.message, extra=_log_extra(request, code=exc.code, errors=exc.errors)) | |
| return create_response(request, exc.to_dict(), status=409) | |
| def handle_rate_limit_exception(request: HttpRequest, exc: RateLimitError) -> HttpResponse: | |
| logger.warning("Rate limited: %s", exc.message, extra=_log_extra(request, code=exc.code)) | |
| response = create_response(request, exc.to_dict(), status=429) | |
| retry_after = _parse_retry_after((exc.errors or {}).get("retry_after")) | |
| if retry_after is not None: | |
| _set_retry_after_header(response, retry_after) | |
| return response | |
| def handle_business_exception(request: HttpRequest, exc: BusinessException) -> HttpResponse: | |
| logger.warning("Business error: %s", exc.message, extra=_log_extra(request, code=exc.code, errors=exc.errors)) | |
| status = int(getattr(exc, "status", 400)) | |
| return create_response(request, exc.to_dict(), status=status) | |
| def handle_business_error(request: HttpRequest, exc: BusinessError) -> HttpResponse: | |
| logger.warning("BusinessError: %s - %s", exc.code, exc.message, extra=_log_extra(request)) | |
| status = int(getattr(exc, "status", 400)) | |
| return create_response(request, exc.to_dict(), status=status) | |
| def _register_server_error_handlers(api: NinjaAPI, create_response: _CreateResponse) -> None: | |
| """注册 5xx 业务异常处理器""" | |
| def handle_service_unavailable_error(request: HttpRequest, exc: ServiceUnavailableError) -> HttpResponse: | |
| logger.error( | |
| "Service unavailable: %s", | |
| exc.message, | |
| extra=_log_extra( | |
| request, | |
| code=exc.code, | |
| errors=exc.errors, | |
| service_name=getattr(exc, "service_name", None), | |
| ), | |
| ) | |
| return create_response(request, exc.to_dict(), status=503) | |
| def handle_recognition_timeout_error(request: HttpRequest, exc: RecognitionTimeoutError) -> HttpResponse: | |
| logger.error( | |
| "Recognition timeout: %s", | |
| exc.message, | |
| extra=_log_extra( | |
| request, | |
| code=exc.code, | |
| errors=exc.errors, | |
| timeout_seconds=getattr(exc, "timeout_seconds", None), | |
| ), | |
| ) | |
| return create_response(request, exc.to_dict(), status=504) | |
| def handle_external_service_error(request: HttpRequest, exc: ExternalServiceError) -> HttpResponse: | |
| logger.error( | |
| "External service error: %s", | |
| exc.message, | |
| extra=_log_extra(request, code=exc.code, errors=exc.errors), | |
| ) | |
| return create_response(request, exc.to_dict(), status=502) | |
| def _register_llm_handlers(api: NinjaAPI, create_response: _CreateResponse) -> None: | |
| """注册 LLM 相关异常处理器(可选依赖,ImportError 时静默跳过)""" | |
| try: | |
| from apps.core.llm.exceptions import LLMAPIError, LLMBackendUnavailableError, LLMTimeoutError | |
| def handle_llm_backend_unavailable_error(request: HttpRequest, exc: LLMBackendUnavailableError) -> HttpResponse: | |
| logger.error( | |
| "LLM backend unavailable: %s", | |
| exc.message, | |
| extra=_log_extra(request, code=exc.code, errors=exc.errors, service_name="llm"), | |
| ) | |
| payload = exc.to_dict() | |
| payload.setdefault("errors", {}) | |
| payload["errors"].setdefault("service", "llm") | |
| return create_response(request, payload, status=503) | |
| def handle_llm_timeout_error(request: HttpRequest, exc: LLMTimeoutError) -> HttpResponse: | |
| logger.error( | |
| "LLM request timeout: %s", | |
| exc.message, | |
| extra=_log_extra(request, code=exc.code, errors=exc.errors), | |
| ) | |
| return create_response(request, exc.to_dict(), status=504) | |
| def handle_llm_api_error(request: HttpRequest, exc: LLMAPIError) -> HttpResponse: | |
| upstream = getattr(exc, "status_code", None) | |
| status_code = _resolve_llm_status_code(upstream) | |
| logger.error( | |
| "LLM API error: %s", | |
| exc.message, | |
| extra=_log_extra(request, code=exc.code, errors=exc.errors, status_code=status_code, upstream=upstream), | |
| ) | |
| return create_response(request, exc.to_dict(), status=status_code) | |
| except ImportError: | |
| pass | |
| except Exception: | |
| logger.exception("Failed to register LLM exception handlers") | |
| def _register_django_handlers(api: NinjaAPI, create_response: _CreateResponse) -> None: | |
| """注册 Django 内置异常处理器""" | |
| def handle_404(request: HttpRequest, exc: Http404) -> HttpResponse: | |
| logger.info("404 Not Found: %s", request.path) | |
| return create_response( | |
| request, | |
| {"code": "NOT_FOUND", "message": "资源不存在", "error": "资源不存在", "errors": {}}, | |
| status=404, | |
| ) | |
| def handle_object_not_exist(request: HttpRequest, exc: ObjectDoesNotExist) -> HttpResponse: | |
| logger.info("Object not found: %s", request.path) | |
| return create_response( | |
| request, | |
| {"code": "NOT_FOUND", "message": "资源不存在", "error": "资源不存在", "errors": {}}, | |
| status=404, | |
| ) | |
| def handle_django_permission_denied(request: HttpRequest, exc: DjangoPermissionDenied) -> HttpResponse: | |
| logger.warning("Permission denied: %s", request.path, extra=_log_extra(request)) | |
| return create_response( | |
| request, | |
| {"code": "PERMISSION_DENIED", "message": "无权限访问", "error": "无权限访问", "errors": {}}, | |
| status=403, | |
| ) | |
| def handle_ninja_validation_error(request: HttpRequest, exc: NinjaValidationError) -> HttpResponse: | |
| logger.info("Validation error: %s", request.path, extra=_log_extra(request, errors=exc.errors)) | |
| return create_response( | |
| request, | |
| {"code": "VALIDATION_ERROR", "message": "数据校验失败", "error": "数据校验失败", "errors": exc.errors}, | |
| status=422, | |
| ) | |
| def handle_http_error(request: HttpRequest, exc: HttpError) -> HttpResponse: | |
| logger.warning("HTTP Error %s: %s", exc.status_code, request.path) | |
| error_message = getattr(exc, "message", None) or str(exc) | |
| if int(exc.status_code) == 429: | |
| return create_response( | |
| request, | |
| {"code": "RATE_LIMIT_ERROR", "message": error_message, "error": error_message, "errors": {}}, | |
| status=429, | |
| ) | |
| return create_response( | |
| request, | |
| {"code": "HTTP_ERROR", "message": error_message, "error": error_message, "errors": {}}, | |
| status=int(exc.status_code), | |
| ) | |
| def _register_jwt_handler(api: NinjaAPI, create_response: _CreateResponse) -> None: | |
| """注册 JWT 异常处理器(可选依赖,ImportError 时静默跳过)""" | |
| try: | |
| from ninja_jwt.exceptions import InvalidToken | |
| def handle_invalid_token(request: HttpRequest, exc: InvalidToken) -> HttpResponse: | |
| logger.info("Invalid token: %s", request.path) | |
| detail = getattr(exc, "detail", {}) | |
| if isinstance(detail, dict): | |
| error_msg = str(detail.get("detail", "令牌无效或已过期")) | |
| else: | |
| error_msg = str(detail) if detail else "令牌无效或已过期" | |
| return create_response( | |
| request, | |
| {"code": "INVALID_TOKEN", "message": error_msg, "error": error_msg, "errors": {}}, | |
| status=401, | |
| ) | |
| except ImportError: | |
| pass | |
| def _register_fallback_handler(api: NinjaAPI, create_response: _CreateResponse) -> None: | |
| """注册兜底异常处理器""" | |
| def handle_unexpected_exception(request: HttpRequest, exc: Exception) -> HttpResponse: | |
| logger.error( | |
| "Unexpected error: %s", | |
| exc, | |
| exc_info=True, | |
| extra=_log_extra(request), | |
| ) | |
| from django.conf import settings | |
| message = str(exc) if settings.DEBUG else "系统错误,请稍后重试" | |
| return create_response( | |
| request, | |
| {"code": "INTERNAL_ERROR", "message": message, "error": message, "errors": {}}, | |
| status=500, | |
| ) | |