docs: 更新 HTTP 错误码、用户积分、占卜运行及用户资料协议文档

This commit is contained in:
qzl
2026-04-10 16:45:45 +08:00
parent 1bc8bc6a27
commit 17ef460391
78 changed files with 18680 additions and 25 deletions
+293
View File
@@ -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
```