Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 130 additions & 81 deletions docs/plans/12-全局响应与异常处理计划.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,128 +4,177 @@ doc_no: "12"
audience:
- Dev
- QA
- Ops
feature_area: global-response-exception-handling
purpose: "实现 PRD07 中的全局响应 envelope、异常处理器、错误模型和日志追踪计划。"
feature_area: global-response-error-handling
purpose: "实现 PRD07 中的全局 failure envelope、exception handlers、request id 集成和契约测试。"
canonical_path: "docs/plans/12-全局响应与异常处理计划.md"
status: draft
version: "1.0.0"
owner: "StephenQiu30"
inputs:
- "docs/prd/07-全局响应格式与异常处理.md"
- "docs/design/02-FastAPI后端结构重设计与规范化方案.md"
- "apps/api/app/core/errors.py"
- "apps/api/app/core/responses.py"
- "apps/api/app/middleware/request_context.py"
outputs:
- "全局响应与异常处理实施计划"
- "全局响应与异常处理计划"
triggers:
- "需要统一 API 错误响应"
- "需要新增全局 exception handler"
- "需要调整 OpenAPI 错误契约"
- "需要规范化全局错误处理"
- "需要集成 request id 到错误响应"
downstream:
- "docs/acceptance/01-个人自部署万能视频下载器验收方案.md"
- "docs/operations/02-OpenAPI契约与前端生成协作.md"
- "openspec/changes/global-response-error-handling/"
---

# PLAN12 全局响应与异常处理

## 1. 背景

PRD07 要求后端成功、失败、日志和 OpenAPI 使用统一错误语义。当前项目已有 `AppError` 和 failure envelope 基础,本计划负责把全局异常处理、request id 和错误模型收束成稳定工程能力
PRD07 定义了全局响应格式和异常处理的需求边界。当前已有初步实现(`AppError`、`ErrorCode`、`failure_response`),但缺少 request id 集成和统一契约测试。本计划将补全这些缺失,确保全局错误处理具备完整的可追踪性和可测试性

## 2. 目标

1. 定义 shared error code 常量和错误响应 schema。
2. 建立 FastAPI 全局 exception handlers。
3. 为 request id 建立响应 meta 和日志链路。
4. 保持既有主链路成功响应兼容。
5. 用契约测试覆盖错误 envelope。
1. 错误响应包含 `request_id` 字段,支持问题追踪。
2. `request_id` 存储在 `request.state` 中,便于全局访问。
3. 所有异常类型(`AppError`、`HTTPException`、`RequestValidationError`、未知异常)遵循统一 failure envelope。
4. 契约测试覆盖所有错误路径。

## 3. 非目标

- 不一次性 envelope 化所有成功响应
- 不修改前端调用方式
- 不改变任务状态机和 Worker 主流程
- 不在本计划中迁移已有成功响应格式
- 不在本计划中添加新的错误码(仅使用已有 `ErrorCode`)
- 不在本计划中修改日志脱敏规则(已在 `unhandled_exception_handler` 中实现基础脱敏)

## 4. 核心内容

### 4.1 文件边界

| 文件 | 职责 |
| --- | --- |
| `packages/shared/video_downloader_shared/error_codes.py` | 定义稳定错误码 |
| `apps/api/app/schemas/responses.py` | 定义 `ErrorResponse`、`ErrorBody`、`ResponseMeta` |
| `apps/api/app/core/errors.py` | 扩展 `AppError` 和领域异常基类 |
| `apps/api/app/core/responses.py` | 生成 success/failure envelope |
| `apps/api/app/core/exception_handlers.py` | 注册 FastAPI 全局异常处理器 |
| `apps/api/app/middleware/request_context.py` | 注入和传播 request id |
| `apps/api/app/main.py` | 挂载 middleware 与 exception handlers |
| `apps/api/tests/test_api_contract.py` | 覆盖错误 envelope 和未知异常 |
| `apps/api/tests/test_request_context_middleware.py` | 覆盖 request id |

### 4.2 执行步骤

1. 新增失败测试:`AppError` 返回 `success=false/error/meta.request_id`。
2. 新增失败测试:`RequestValidationError` 返回 `validation_error`。
3. 新增失败测试:未知异常返回 `internal_error` 且不泄露原文。
4. 新增 shared error code 常量。
5. 新增响应 schema 和 response helper。
6. 新增或收束全局 exception handlers。
7. 调整 request context middleware,保证响应和日志能读取 request id。
8. 更新 OpenAPI 契约测试,确认错误模型可见。
9. 保持既有成功响应测试不变,避免兼容破坏。

### 4.3 测试命令

```bash
pytest apps/api/tests/test_api_contract.py -v
pytest apps/api/tests/test_request_context_middleware.py -v
pytest apps/api/tests/test_task_endpoints.py -v
pytest apps/api/tests/test_auth.py -v
### 4.1 Request ID 集成

**修改文件:** `apps/api/app/middleware/request_context.py`

**变更内容:**

1. 将 `request_id` 存储到 `request.state.request_id`
2. 保持现有响应头 `X-Request-ID` 逻辑不变

**实现步骤:**

```python
# 在 call_next 之前添加
request.state.request_id = request_id
```

### 4.4 提交顺序
### 4.2 Failure Envelope 增强

**修改文件:** `apps/api/app/core/responses.py`

1. `test:` 全局错误 envelope 和 request id 失败测试。
2. `impl:` shared error code、response schema 和 exception handlers。
3. `impl:` request id meta 和日志字段收束。
4. `docs:` 同步 OpenAPI 协作和验收说明。
**变更内容:**

## 5. 关联文档
1. `failure_response` 函数新增 `request_id` 参数
2. 错误响应结构包含 `request_id` 字段

### 5.1 输入文档
**实现步骤:**

1. `docs/prd/07-全局响应格式与异常处理.md`
2. `docs/design/02-FastAPI后端结构重设计与规范化方案.md`
```python
def failure_response(code: str, message: str, details: Any = None, request_id: str | None = None) -> dict[str, Any]:
error = {
"code": code,
"message": message,
"details": details,
}
if request_id:
error["request_id"] = request_id
return {
"success": False,
"error": error,
}
```

### 5.2 输出文档
### 4.3 Exception Handlers 更新

1. `packages/shared/video_downloader_shared/error_codes.py`
2. `apps/api/app/schemas/responses.py`
3. `apps/api/app/core/exception_handlers.py`
**修改文件:** `apps/api/app/core/errors.py`

### 5.3 下游文档
**变更内容:**

1. 所有 exception handler 从 `request.state` 获取 `request_id`
2. 将 `request_id` 传递给 `failure_response`

**实现步骤:**

1. `app_error_handler`:获取 `request_id` 并传递
2. `http_exception_handler`:获取 `request_id` 并传递
3. `validation_exception_handler`:获取 `request_id` 并传递
4. `unhandled_exception_handler`:获取 `request_id` 并传递

**示例:**

```python
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
request_id = getattr(request.state, "request_id", None)
return JSONResponse(
status_code=exc.status_code,
content=failure_response(exc.code, exc.message, exc.details, request_id),
)
```

1. `docs/acceptance/01-个人自部署万能视频下载器验收方案.md`
2. `docs/operations/02-OpenAPI契约与前端生成协作.md`
### 4.4 契约测试

## 6. 验收门禁
**新增文件:** `apps/api/tests/test_error_contract.py`

- `AppError`、参数校验错误和未知异常返回统一 failure envelope。
- 错误响应包含 `success=false`、`error.code`、`error.message`、`error.details` 和 `meta.request_id`。
- 未知异常不向响应泄露内部异常原文。
- 既有成功响应兼容测试通过。
- 指定测试命令通过。
**测试用例:**

1. **AppError 测试**
- 验证 `AppError` 响应包含 `success: false`
- 验证响应包含 `error.code` 和 `error.message`
- 验证响应包含 `request_id`

2. **HTTPException 测试**
- 验证 401/403/404/5xx 映射为标准错误码
- 验证响应包含 `request_id`

3. **RequestValidationError 测试**
- 验证参数校验错误返回 `validation_error`
- 验证 `details` 包含校验错误详情
- 验证响应包含 `request_id`

4. **未知异常测试**
- 验证未知异常返回 `internal_error`
- 验证不暴露内部堆栈
- 验证响应包含 `request_id`

5. **Request ID 来源测试**
- 验证优先使用客户端传入的 `X-Request-ID`
- 验证未传入时自动生成 UUID

## 5. 任务拆解

### 5.1 实施任务

| 序号 | 任务 | 文件 | 说明 |
| --- | --- | --- | --- |
| 1 | Request ID 存储到 request.state | `middleware/request_context.py` | 在 middleware 中存储 request_id |
| 2 | failure_response 增加 request_id | `core/responses.py` | 函数签名和实现更新 |
| 3 | Exception handlers 传递 request_id | `core/errors.py` | 4 个 handler 更新 |
| 4 | 契约测试 | `tests/test_error_contract.py` | 新增测试文件 |

### 5.2 验证任务

| 序号 | 验证项 | 命令 | 预期结果 |
| --- | --- | --- | --- |
| 1 | 单元测试 | `pytest apps/api/tests/test_error_contract.py -v` | 全部通过 |
| 2 | 集成测试 | `pytest apps/api/tests/ -v` | 无回归 |
| 3 | 仓库验证 | `bash scripts/validate-repository.sh` | 通过 |

## 7. 风险与边界
## 6. 风险与边界

如果全量切换成功响应 envelope,调用方需要同步调整。该计划只要求错误响应统一和新增 API 优先 envelope 化。
- **风险**:修改 `failure_response` 签名可能影响已有调用 → **缓解**:`request_id` 参数默认值为 `None`,保持向后兼容
- **风险**:`request.state` 在某些异常场景下可能不存在 → **缓解**:使用 `getattr(request.state, "request_id", None)` 安全访问

## 8. 待确认问题
## 7. 依赖关系

- 是否为 `details` 建立平台级字段白名单。
- 是否在 Nginx 或网关层透传外部 request id。
- 依赖 `apps/api/app/core/errors.py` 已有实现
- 依赖 `apps/api/app/core/responses.py` 已有实现
- 依赖 `apps/api/app/middleware/request_context.py` 已有实现

## 9. 变更记录
## 8. 变更记录

| 日期 | 作者 | 版本 | 变更说明 |
| --- | --- | --- | --- |
| 2026-06-16 | StephenQiu30 | 1.0.0 | 初始化 PLAN12 |
| 2026-06-16 | StephenQiu30 | 1.0.0 | 创建 PLAN12:全局响应与异常处理计划 |
Loading
Loading