diff --git a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md index 199fa67b..dfa3bafb 100644 --- a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md +++ b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md @@ -14,7 +14,8 @@ 交付物:[M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md) - [x] 整理当前所有 `/generated-*` 静态资源前缀 交付物:[M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md](./M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md) -- [ ] 整理当前前端直接依赖的响应头、envelope、错误格式 +- [x] 整理当前前端直接依赖的响应头、envelope、错误格式 + 交付物:[M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md](./M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md) ### 仓库边界 diff --git a/backend-rewrite-tasklist/M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md b/backend-rewrite-tasklist/M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md new file mode 100644 index 00000000..3af1cdf2 --- /dev/null +++ b/backend-rewrite-tasklist/M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md @@ -0,0 +1,262 @@ +# M0:前端直接依赖的响应头、Envelope 与错误格式冻结基线 + +日期:`2026-04-20` + +依据来源: + +- `server-node/src/http.ts` +- `server-node/src/middleware/responseEnvelope.ts` +- `server-node/src/middleware/errorHandler.ts` +- `server-node/src/middleware/requestId.ts` +- `packages/shared/src/http.ts` +- `src/services/apiClient.ts` +- `src/services/authService.ts` +- `src/services/aiService.ts` +- `src/editor/shared/jsonClient.ts` +- `src/services/apiClient.test.ts` + +## 1. 文档目的 + +这份文档用于完成 `M0` 的第六条任务: + +- 整理当前前端直接依赖的响应头、envelope、错误格式 + +这里的“直接依赖”指的是:如果 Axum 重写时把这些头或 body 结构改掉,当前前端 `src/services/*`、编辑器请求层和鉴权异常处理就会立刻出问题。 + +## 2. 冻结结论 + +当前前端直接依赖的响应契约,冻结为以下 4 层: + +1. 请求侧默认会发送 `x-genarrative-response-envelope: v1`。 +2. 响应侧默认要回 `x-request-id`、`x-api-version`、`x-route-version`。 +3. 成功响应在请求方要求 envelope 时,必须返回标准 `ok/data/error/meta` 结构。 +4. 错误响应既要兼容标准 envelope,也要兼容旧式 `{ error, meta }` / `{ message, code }` 解析回退。 + +补充结论: + +1. 当前正式前端代码里,没有生产用例主动关闭 envelope 请求头。 +2. `x-response-time-ms` 当前不是前端代码的直接读取项,但属于现有兼容头集合,重写时仍应保留。 +3. 鉴权链路额外直接依赖错误码 `CAPTCHA_REQUIRED` 与 `error.details.captchaChallenge`。 + +## 3. 当前前端直接依赖矩阵 + +| 依赖项 | 当前值/结构 | 当前消费者 | 当前作用 | +| --- | --- | --- | --- | +| 请求头 | `x-genarrative-response-envelope: v1` | `src/services/apiClient.ts`、`src/editor/shared/jsonClient.ts` | 请求标准 envelope 响应。 | +| 响应头 | `x-request-id` | `src/services/apiClient.ts` | 构造 `ApiClientError.meta.requestId` 的回退来源。 | +| 响应头 | `x-api-version` | `src/services/apiClient.ts`、`packages/shared/src/http.ts` | 识别标准 envelope / error body。 | +| 响应头 | `x-route-version` | `src/services/apiClient.ts` | 构造 `ApiClientError.meta.routeVersion` 的回退来源。 | +| 成功 body | `{ ok: true, data, error: null, meta }` | `unwrapApiResponse(...)` | 前端默认解包标准成功 envelope。 | +| 错误 body | `{ ok: false, data: null, error, meta }` | `ApiClientError`、`parseApiErrorMessage(...)` | 标准错误解析。 | +| 旧错误 body | `{ error, meta }` / `{ message, code }` | `parseApiErrorMessage(...)` | 老接口或非标准错误回退解析。 | +| 错误细节 | `error.code === 'CAPTCHA_REQUIRED'` 且 `error.details.captchaChallenge` | `src/services/authService.ts` | 手机验证码发送前的验证码挑战弹出。 | + +## 4. 请求侧冻结要求 + +### 4.1 Envelope 请求头 + +当前前端默认行为: + +1. `src/services/apiClient.ts` 会自动补: + - `x-genarrative-response-envelope: v1` +2. `src/editor/shared/jsonClient.ts` 也会自动补: + - `x-genarrative-response-envelope: v1` + +当前后端接受的 envelope 触发值: + +1. `1` +2. `true` +3. `v1` +4. `envelope` + +但当前前端真实发送值冻结为: + +1. `v1` + +补充冻结点: + +1. 虽然 `apiClient` 提供了 `omitEnvelopeHeader` 选项,但当前生产代码没有实际依赖它。 +2. 因此第一阶段 Axum 应默认兼容“前端请求即要 envelope”的模式。 + +### 4.2 鉴权与凭证约定 + +当前前端请求层默认还会做: + +1. `Authorization: Bearer ` 自动注入。 +2. `credentials: same-origin`。 +3. 遇到 `401` 时尝试走 `/api/auth/refresh` 自动刷新。 + +这不是本文重点,但它解释了为什么 envelope 和错误格式必须在 `/api/auth/refresh` 上也保持兼容。 + +## 5. 响应头冻结要求 + +### 5.1 必须保留的前端直接依赖头 + +| 响应头 | 当前来源 | 当前前端用法 | +| --- | --- | --- | +| `x-request-id` | `requestIdMiddleware` + `applyApiResponseHeaders` | `ApiClientError.meta.requestId` 的 header 回退来源。 | +| `x-api-version` | `applyApiResponseHeaders` | 当前标准 API 契约版本识别。 | +| `x-route-version` | `applyApiResponseHeaders` | `ApiClientError.meta.routeVersion` 的 header 回退来源。 | + +### 5.2 兼容头但非直接读取项 + +| 响应头 | 当前状态 | 说明 | +| --- | --- | --- | +| `x-response-time-ms` | 当前统一输出 | 目前前端代码未直接读取,但设计文档与联调约定已锁定,不能随意删除。 | + +补充冻结点: + +1. `requestIdMiddleware` 会优先回显请求方传入的 `x-request-id`,否则服务端自生成。 +2. `ApiClientError` 读取元信息时优先取 body `meta`,没有再回退到 headers。 +3. 这意味着即便 envelope body 缺少部分 `meta` 字段,headers 仍必须完整。 + +## 6. 成功响应 Envelope 冻结格式 + +当前标准成功 envelope: + +```json +{ + "ok": true, + "data": {}, + "error": null, + "meta": { + "apiVersion": "2026-04-08", + "requestId": "req-xxx", + "routeVersion": "2026-04-08", + "operation": "runtime.story.initial", + "latencyMs": 12, + "timestamp": "2026-04-20T00:00:00.000Z" + } +} +``` + +冻结规则: + +1. `ok` 必须为 `true`。 +2. `data` 为真实业务负载。 +3. `error` 必须为 `null`。 +4. `meta.apiVersion` 必须存在,因为 `unwrapApiResponse(...)` 与 `isApiResponse(...)` 依赖它判断标准 envelope。 + +补充说明: + +1. 如果请求未带 envelope 头,当前后端可以直接返回裸 `data`。 +2. 但由于当前前端默认都会请求 envelope,第一阶段 Axum 基本等价于“所有 JSON 成功响应都要兼容这个结构”。 + +## 7. 错误响应 Envelope 与旧格式回退 + +### 7.1 当前标准错误 envelope + +```json +{ + "ok": false, + "data": null, + "error": { + "code": "UNAUTHORIZED", + "message": "缺少 Authorization Bearer Token", + "details": null + }, + "meta": { + "apiVersion": "2026-04-08", + "requestId": "req-xxx", + "routeVersion": "2026-04-08", + "operation": "auth.me", + "latencyMs": 3, + "timestamp": "2026-04-20T00:00:00.000Z" + } +} +``` + +冻结规则: + +1. `ok` 必须为 `false`。 +2. `data` 必须为 `null`。 +3. `error.code`、`error.message` 必须存在。 +4. `error.details` 可为对象或 `null`。 +5. `meta.apiVersion` 必须存在。 + +### 7.2 当前旧式错误格式回退 + +当请求未要求 envelope,或某些链路仍走旧写法时,当前后端与前端仍兼容以下错误结构: + +1. `{ "error": { "code": "...", "message": "...", "details": ... }, "meta": {...} }` +2. `{ "message": "...", "code": "..." }` +3. `{ "error": { "message": "..." } }` +4. 纯文本错误响应 + +`parseApiErrorMessage(rawText, fallbackMessage)` 的当前回退顺序固定为: + +1. `parsed.error.message` +2. 顶层 `message` +3. `error.code` 或顶层 `code`,拼成 `fallback(CODE)` +4. 原始文本 +5. 调用方的 `fallbackMessage` + +这意味着: + +1. Axum 第一阶段不能只兼容标准 envelope,而忽略旧错误解析的回退行为。 +2. 至少在迁移过渡期,`parseApiErrorMessage(...)` 可识别的信息要继续保留。 + +## 8. 前端对错误细节的业务级直接依赖 + +### 8.1 验证码挑战 + +`src/services/authService.ts` 当前明确依赖: + +1. `error instanceof ApiClientError` +2. `error.code === 'CAPTCHA_REQUIRED'` +3. `error.details.captchaChallenge` + +冻结要求: + +1. 如果后端要继续触发验证码挑战,必须继续返回: + - `code: 'CAPTCHA_REQUIRED'` + - `details.captchaChallenge` +2. 不能只返回中文文案而不带结构化 `details`。 + +### 8.2 元信息透传 + +`ApiClientError` 当前会保留: + +1. `status` +2. `code` +3. `details` +4. `meta.apiVersion` +5. `meta.requestId` +6. `meta.routeVersion` +7. `meta.operation` +8. `meta.latencyMs` +9. `meta.timestamp` + +冻结要求: + +1. Axum 不能把这些字段全都删成单纯 `message` 字符串。 +2. 即使部分业务 UI 现在没显示这些字段,它们已经进入前端错误对象结构。 + +## 9. 当前消费者清单 + +以下文件已构成当前前端的直接依赖面: + +1. `src/services/apiClient.ts` +2. `src/services/authService.ts` +3. `src/services/aiService.ts` +4. `src/editor/shared/jsonClient.ts` +5. `packages/shared/src/http.ts` + +## 10. 本轮冻结后的硬约束 + +后续迁移中,不允许出现以下情况: + +1. 删除 `x-genarrative-response-envelope: v1` 的请求协商能力。 +2. 删除 `x-request-id`、`x-api-version`、`x-route-version` 这些当前前端直接依赖的响应头。 +3. 把成功 envelope 从 `{ ok, data, error, meta }` 改成其他字段名。 +4. 把错误 envelope 从 `{ ok: false, data: null, error, meta }` 改成只有 `message`。 +5. 删除 `CAPTCHA_REQUIRED + details.captchaChallenge` 这一结构化错误契约。 +6. 让前端默认请求 envelope,但后端返回裸数据且不再可被 `unwrapApiResponse(...)` 识别。 + +## 11. 本任务完成定义 + +当以下条件成立时,这条任务视为完成: + +1. 当前前端直接依赖的响应头、envelope、错误格式已有书面冻结清单。 +2. 已明确哪些是前端直接读取项,哪些是兼容保留项。 +3. 后续 Axum handler、错误中间件、response envelope 中间件可以直接按本文对齐,而不再靠人工试错。 diff --git a/backend-rewrite-tasklist/README.md b/backend-rewrite-tasklist/README.md index fd53c811..f83027f3 100644 --- a/backend-rewrite-tasklist/README.md +++ b/backend-rewrite-tasklist/README.md @@ -19,6 +19,7 @@ - [M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md):当前 `12` 个内部模块的迁移归属基线,用于锁定 Rust crate、SpacetimeDB bounded context 与 Axum/application 分工。 - [M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md):当前 `6` 条 SSE 接口及其事件格式冻结基线,用于 Axum SSE 兼容和前端 contract 回归。 - [M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md](./M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md):当前正式 `/generated-*` 静态资源前缀冻结基线,用于 Axum 静态资源兼容层与 OSS 对象键规划。 +- [M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md](./M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md):当前前端直接依赖的响应头、envelope 与错误格式冻结基线,用于 Axum 中间件与错误响应兼容。 ## 维护规则