docs: add frontend response contract baseline
This commit is contained in:
@@ -14,7 +14,8 @@
|
|||||||
交付物:[M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md)
|
交付物:[M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md)
|
||||||
- [x] 整理当前所有 `/generated-*` 静态资源前缀
|
- [x] 整理当前所有 `/generated-*` 静态资源前缀
|
||||||
交付物:[M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md](./M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md)
|
交付物:[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)
|
||||||
|
|
||||||
### 仓库边界
|
### 仓库边界
|
||||||
|
|
||||||
|
|||||||
@@ -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 <token>` 自动注入。
|
||||||
|
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 中间件可以直接按本文对齐,而不再靠人工试错。
|
||||||
@@ -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_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_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_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 中间件与错误响应兼容。
|
||||||
|
|
||||||
## 维护规则
|
## 维护规则
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user