# Error Handling > How errors are handled in this project. --- ## Overview This project follows **RFC 7807 Problem Details** for HTTP API errors: - **Standard format**: `application/problem+json` - **Error codes**: Machine-readable `UPPER_SNAKE_CASE` - **Source of truth**: `docs/protocols/common/http-error-codes.md` - **Exception class**: `ApiProblemError` for domain errors - **Layered approach**: Routers handle HTTP, Services raise domain errors --- ## Error Types ### `ApiProblemError` **Custom exception for business logic errors:** ```python from core.http.errors import ApiProblemError, problem_payload # Raise in service/repository/dependencies raise ApiProblemError( status_code=403, detail=problem_payload(code="AGENT_FORBIDDEN", detail="Forbidden"), ) # With params raise ApiProblemError( status_code=422, detail=problem_payload( code="INVALID_RUNTIME_MODE", detail="Invalid runtime mode", params={"mode": mode, "valid_modes": ["standard", "divination"]} ), ) ``` **Properties:** - `status_code`: HTTP status (int) - `code`: Machine-readable error code (str, `UPPER_SNAKE_CASE`) - `detail`: Human-readable message (str) - `params`: Optional additional context (dict) ### `HTTPException` (FastAPI) **Use ONLY in router layer for HTTP transport errors:** ```python from fastapi import HTTPException, status # Only in routers, for simple HTTP errors raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Resource not found" ) ``` **For business logic → use `ApiProblemError` instead.** --- ## Error Handling Patterns ### Service Layer (Business Logic) **Services should raise `ApiProblemError`:** ```python class AgentService: async def enqueue_run(self, *, run_input: RunAgentInput, current_user: CurrentUser) -> TaskAccepted: # Validate business rules if owner_id != str(current_user.id): raise ApiProblemError( status_code=403, detail=problem_payload( code="AGENT_FORBIDDEN", detail="Forbidden" ), ) # Return success result return TaskAccepted(run_id=run_id, thread_id=thread_id) ``` ### Repository Layer (Data Access) **Re-raise exceptions as `ApiProblemError` when needed:** ```python from sqlalchemy.exc import IntegrityError from core.http.errors import ApiProblemError, problem_payload class AgentRepository(BaseRepository[AgentChatSession]): async def create_session(self, ...) -> AgentChatSession: try: self._session.add(session) await self._session.flush() return session except IntegrityError as exc: raise ApiProblemError( status_code=409, detail=problem_payload( code="DUPLICATE_SESSION", detail="Session already exists" ), ) from exc ``` ### Router Layer (HTTP Endpoints) **Routers handle `ApiProblemError` and convert to HTTP response:** ```python from fastapi import APIRouter, HTTPException from core.http.errors import ApiProblemError router = APIRouter() @router.post("/agent/run") async def create_run( response: Response, current_user: CurrentUser = Depends(get_current_user), service: AgentService = Depends(get_agent_service), ): try: result = await service.enqueue_run(run_input=input, current_user=current_user) return result except ApiProblemError as exc: response.status_code = exc.status_code return { "code": exc.code, "detail": exc.detail, **({"params": exc.params} if exc.params else {}), } ``` --- ## API Error Responses ### RFC 7807 Format **All API errors return `application/problem+json`:** ```json { "code": "AGENT_FORBIDDEN", "detail": "Forbidden", "params": { "resource": "agent_session", "action": "update" } } ``` **Error code registry:** - **Single source of truth**: `docs/protocols/common/http-error-codes.md` - **Update requirement**: Any create/modify/deprecate of codes requires doc update ### Error Response Construction **Use `problem_payload()` helper:** ```python from core.http.errors import problem_payload # Simple error detail = problem_payload( code="INVALID_INPUT", detail="Invalid input data" ) # With params detail = problem_payload( code="VALIDATION_ERROR", detail="Validation failed", params={"field": "email", "reason": "invalid format"} ) # Pass to ApiProblemError raise ApiProblemError(status_code=422, detail=detail) ``` --- ## Common Mistakes ### ❌ Catching and ignoring exceptions ```python # WRONG: Silent failure destroys debuggability try: await service.do_something() except Exception: pass # Never do this ``` **Right way:** Re-raise or convert to typed error: ```python try: await service.do_something() except SomeSpecificError as exc: raise ApiProblemError( status_code=400, detail=problem_payload(code="OPERATION_FAILED", detail=str(exc)), ) from exc ``` ### ❌ Using `print()` for errors ```python # WRONG: Use logging except Exception as e: print(f"Error: {e}") # Never use print in runtime code ``` **Right way:** Use `core.logging`: ```python from core.logging import get_logger logger = get_logger(__name__) try: await service.do_something() except Exception as e: logger.error("operation_failed", error=str(e), exc_info=True) raise ``` ### ❌ Using free-text `detail` as only error contract ```python # WRONG: No machine-readable code raise HTTPException(status_code=400, detail="Something went wrong") ``` **Right way:** Use `code` field: ```python raise ApiProblemError( status_code=400, detail=problem_payload(code="OPERATION_FAILED", detail="Something went wrong"), ) ``` ### ❌ Using `HTTPException` in service/repository layer ```python # WRONG: HTTP-specific error in business logic class AgentService: async def enqueue_run(self, ...): raise HTTPException(status_code=403, detail="Forbidden") # Coupled to HTTP ``` **Right way:** Use domain error: ```python class AgentService: async def enqueue_run(self, ...): raise ApiProblemError( status_code=403, detail=problem_payload(code="AGENT_FORBIDDEN", detail="Forbidden"), ) ``` ### ❌ Not logging exceptions ```python # WRONG: Exception is caught but not logged try: await service.do_something() except Exception: raise ApiProblemError(...) # Where did it come from? ``` **Right way:** Log before re-raising: ```python try: await service.do_something() except Exception as exc: logger.error("operation_failed", error=str(exc), exc_info=True) raise ApiProblemError(...) from exc ```