docs: 更新 HTTP 错误码、用户积分、占卜运行及用户资料协议文档
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user