Spaces:
Running
Running
Download src/services/api/errors.py from Leon4gr45/brain: direct link, hf CLI and curl.
- Browser
- Download file 8.16 kB
-
https://huggingface.co/spaces/Leon4gr45/brain/resolve/main/src/services/api/errors.py
- Command line
-
hf download hf://spaces/Leon4gr45/brain/src/services/api/errors.py
-
curl -L -o errors.py https://huggingface.co/spaces/Leon4gr45/brain/resolve/main/src/services/api/errors.py
8.16 kB
| """Structured, agent-readable HTTP error responses.""" | |
| from __future__ import annotations | |
| from collections.abc import Mapping | |
| from typing import Any | |
| from uuid import uuid4 | |
| from fastapi import FastAPI, Request | |
| from fastapi.encoders import jsonable_encoder | |
| from src.services.redaction import redact | |
| from fastapi.exceptions import RequestValidationError | |
| from fastapi.responses import JSONResponse | |
| from pydantic import BaseModel, Field, JsonValue | |
| from starlette.exceptions import HTTPException as StarletteHTTPException | |
| from src.lib.embeddings.profile import EmbeddingProfileError | |
| from src.services.brain_identity import BrainNotFoundError, BrainAlreadyExistsError, InvalidBrainIdError | |
| class ErrorInfo(BaseModel): | |
| """Stable machine-readable information attached to every API error.""" | |
| code: str = Field(description="Stable error code for programmatic handling.") | |
| message: str = Field(description="Short human-readable error summary.") | |
| resolution: str = Field(description="Concrete guidance for resolving the error.") | |
| request_id: str = Field(description="Identifier shared with the X-Request-ID header.") | |
| class ErrorResponse(BaseModel): | |
| """Additive error envelope that preserves FastAPI's legacy detail field.""" | |
| detail: JsonValue = Field(description="Original FastAPI error detail value.") | |
| error: ErrorInfo | |
| _STATUS_DEFAULTS: dict[int, tuple[str, str, str]] = { | |
| 400: ( | |
| "INVALID_REQUEST", | |
| "The request is invalid.", | |
| "Correct the request parameters and retry.", | |
| ), | |
| 401: ( | |
| "AUTH_INVALID", | |
| "Authentication failed.", | |
| "Send a valid BrainPAT or Bearer token for the selected brain.", | |
| ), | |
| 403: ( | |
| "FORBIDDEN", | |
| "The operation is not permitted.", | |
| "Use credentials with permission for this operation.", | |
| ), | |
| 404: ( | |
| "RESOURCE_NOT_FOUND", | |
| "The requested resource was not found.", | |
| "Check the path, identifier, and selected brain before retrying.", | |
| ), | |
| 405: ( | |
| "METHOD_NOT_ALLOWED", | |
| "The HTTP method is not allowed for this resource.", | |
| "Use one of the methods documented in the OpenAPI specification.", | |
| ), | |
| 406: ( | |
| "BRAIN_NOT_FOUND", | |
| "The selected brain could not be resolved.", | |
| "Create the brain or provide the correct X-Brain-ID value.", | |
| ), | |
| 422: ( | |
| "VALIDATION_ERROR", | |
| "Request validation failed.", | |
| "Correct the fields listed in detail and retry.", | |
| ), | |
| 429: ( | |
| "RATE_LIMITED", | |
| "The request rate limit was exceeded.", | |
| "Wait before retrying and use bounded exponential backoff.", | |
| ), | |
| 500: ( | |
| "INTERNAL_ERROR", | |
| "The service could not complete the request.", | |
| "Retry only when safe; contact support with the request ID if it persists.", | |
| ), | |
| 501: ( | |
| "not_implemented", | |
| "The operation is not implemented.", | |
| "Use a supported operation or consult the release notes for availability.", | |
| ), | |
| 503: ( | |
| "SERVICE_UNAVAILABLE", | |
| "A required service is temporarily unavailable.", | |
| "Retry with bounded exponential backoff after the dependency recovers.", | |
| ), | |
| } | |
| def _request_id(request: Request) -> str: | |
| return ( | |
| request.headers.get("X-Request-ID") | |
| or request.headers.get("X-Trace-ID") | |
| or str(uuid4()) | |
| ) | |
| def error_response( | |
| request: Request, | |
| *, | |
| status_code: int, | |
| detail: Any, | |
| code: str | None = None, | |
| message: str | None = None, | |
| resolution: str | None = None, | |
| extra: dict[str, Any] | None = None, | |
| headers: Mapping[str, str] | None = None, | |
| ) -> JSONResponse: | |
| """Create the common JSON envelope while retaining the legacy detail value.""" | |
| default_code, default_message, default_resolution = _STATUS_DEFAULTS.get( | |
| status_code, | |
| _STATUS_DEFAULTS[500], | |
| ) | |
| request_id = _request_id(request) | |
| content: dict[str, Any] = { | |
| "detail": detail, | |
| "error": { | |
| "code": code or default_code, | |
| "message": message or default_message, | |
| "resolution": resolution or default_resolution, | |
| "request_id": request_id, | |
| }, | |
| } | |
| if extra: | |
| content.update(extra) | |
| response_headers = dict(headers or {}) | |
| response_headers["X-Request-ID"] = request_id | |
| return JSONResponse( | |
| status_code=status_code, | |
| content=content, | |
| headers=response_headers, | |
| ) | |
| def install_error_handlers(app: FastAPI) -> None: | |
| """Install handlers for framework, validation, and unexpected exceptions.""" | |
| async def http_exception_handler(request: Request, exc: StarletteHTTPException): | |
| return error_response( | |
| request, | |
| status_code=exc.status_code, | |
| detail=exc.detail, | |
| headers=exc.headers, | |
| ) | |
| async def validation_exception_handler( | |
| request: Request, | |
| exc: RequestValidationError, | |
| ): | |
| return error_response( | |
| request, | |
| status_code=422, | |
| # Pydantic puts exception objects in ctx.error; input may contain | |
| # credentials and is unnecessary for actionable field diagnostics. | |
| detail=redact(jsonable_encoder( | |
| [{k: v for k, v in error.items() if k != "input"} for error in exc.errors()], | |
| custom_encoder={ValueError: str, TypeError: str, Exception: str}, | |
| )), | |
| ) | |
| async def missing_brain_handler(request: Request, _exc: BrainNotFoundError): | |
| return error_response( | |
| request, status_code=404, detail="Brain not found", code="BRAIN_NOT_FOUND", | |
| resolution="Create the brain explicitly or select a registered brain.", | |
| ) | |
| async def duplicate_brain_handler(request: Request, _exc: BrainAlreadyExistsError): | |
| return error_response( | |
| request, status_code=409, detail="Brain already exists", code="BRAIN_ALREADY_EXISTS", | |
| message="A brain with this identifier already exists.", | |
| resolution="Select the existing brain or create a different identifier.", | |
| ) | |
| async def invalid_brain_handler(request: Request, exc: InvalidBrainIdError): | |
| return error_response(request, status_code=400, detail=str(exc), code=exc.code) | |
| async def embedding_profile_exception_handler(request: Request, exc: EmbeddingProfileError): | |
| return error_response( | |
| request, | |
| status_code=503, | |
| detail=str(exc), | |
| code="EMBEDDING_PROFILE_MISMATCH", | |
| message="The configured embedding profile does not match this index.", | |
| resolution="Use a new empty brain/index or explicitly re-index its source texts with the configured model.", | |
| ) | |
| async def unexpected_exception_handler(request: Request, _exc: Exception): | |
| return error_response( | |
| request, | |
| status_code=500, | |
| detail="Internal server error", | |
| ) | |
| COMMON_ERROR_RESPONSES = { | |
| 400: {"model": ErrorResponse, "description": "Invalid request"}, | |
| 401: {"model": ErrorResponse, "description": "Authentication failed"}, | |
| 403: {"model": ErrorResponse, "description": "Operation forbidden"}, | |
| 404: {"model": ErrorResponse, "description": "Resource not found"}, | |
| 405: {"model": ErrorResponse, "description": "Method not allowed"}, | |
| 406: {"model": ErrorResponse, "description": "Brain not found"}, | |
| 422: {"model": ErrorResponse, "description": "Validation failed"}, | |
| 429: {"model": ErrorResponse, "description": "Rate limit exceeded"}, | |
| 500: {"model": ErrorResponse, "description": "Internal service error"}, | |
| 501: {"model": ErrorResponse, "description": "Operation not implemented"}, | |
| 503: {"model": ErrorResponse, "description": "Dependency unavailable"}, | |
| } | |