后端面试:如何用 RFC 9457 统一 HTTP API 错误?
面试官考察点
一个多租户 API 的错误响应来自网关、应用和异步任务,客户端难以稳定解析。请基于 RFC 9457 设计统一错误契约,并说明状态码、媒体类型、错误类型 URI、批量校验、重试、日志脱敏和版本兼容。
背景与约束
- 同一请求可能经过 CDN、API 网关、业务服务和任务队列。
- 客户端需要区分可修正的输入错误、权限问题、限流和暂时性故障。
- 错误详情不能泄露堆栈、密钥、租户隔离信息或内部主机名。
- 旧客户端仍会解析已有字段,不能突然把所有失败改成新的业务状态码。
先分离 HTTP 语义与业务细节
HTTP 状态码表达请求在协议层的结果,Problem Details 负载解释具体原因。400、401、403、404、409、429 和 5xx 仍按语义选择,不能把所有失败都包装成 200。错误类型用稳定 URI 标识可机器识别的类别,title 可供展示,detail 只能描述当前实例。
建立最小且可扩展的字段契约
核心字段包括 type、title、status、detail 和 instance。业务扩展字段应使用明确命名空间,例如 errors 表示字段级问题、retryAfter 表示客户端等待建议。type 的文档页应说明语义、适用状态码和可行动作;客户端不要依赖 title 的自然语言。
让网关与应用共享错误边界
网关生成的超时、认证和限流错误也使用相同媒体类型,但不能伪造应用的业务 type。应用向上游保留可观测的内部错误码,向外只返回允许公开的 Problem Details。跨服务传播时携带关联 ID,不复制敏感的 detail。
回答前需要澄清的问题
- 客户端是按状态码、
type还是旧版code字段分支?这决定兼容层和迁移顺序。 - 是否需要单次响应返回多个字段校验错误?这决定
errors的结构和顺序保证。 - 网关是否能读取业务错误,还是只负责传输和生成基础错误?这决定类型 URI 的所有权。
30 秒回答框架
“我会保留正确的 HTTP 状态码,再用 application/problem+json 返回稳定的 type、title、status、detail 和可选 instance。客户端按 type 和状态码处理,不能依赖文案;网关只生成它负责的错误类型。字段校验、重试提示、关联 ID 和脱敏规则进入版本化契约,并用兼容矩阵和真实链路验证。”
分步骤深入解答
先定义错误类型注册表:每个类型有 URI、公开字段、允许状态码、客户端动作和安全等级。请求校验失败返回 400,字段问题集中放入 errors;身份缺失和权限不足区分 401 与 403;并发版本冲突用 409;限流返回 429 并在响应头和扩展字段中给出等待建议;未知故障返回 500 或 503,避免把内部异常类型暴露给调用方。
响应头的 Content-Type 必须与负载一致。错误体可以包含 instance 作为一次请求的追踪引用,但不能把完整 URL、SQL、堆栈或租户标识直接放进 detail。服务端日志保存内部原因、关联 ID 和安全审计字段,客户端只看到经过策略过滤的内容。
对于批量校验,errors 使用字段路径到问题列表的结构,并规定是否允许多个问题、字段路径语法和最大数量。客户端未知扩展字段时应忽略;服务端新增字段只能向后兼容,改变既有 type 语义则发布新 URI。异步任务失败通过任务资源状态返回 Problem Details,不把队列内部异常直接同步给用户。
高质量示范回答
“我会维护一个错误类型注册表,并让网关、同步服务和异步任务都输出 RFC 9457 兼容的 application/problem+json。状态码表达协议语义,type 表达稳定类别,detail 只描述当前请求。字段校验使用受限的 errors 扩展;429 同时提供可解析的等待提示;500 和 503 使用通用公开类型,内部堆栈只进日志。迁移时保留旧 code,通过契约测试、客户端兼容矩阵和脱敏审计逐步切换。”
常见错误
- 用 200 搭配业务失败字段,让缓存、监控和重试器误判成功。
- 让客户端按
title或detail文案分支,导致翻译和措辞变更即破坏兼容。 - 每个服务随意定义
type,同一语义出现多个 URI,无法统一统计。 - 在
detail中返回堆栈、SQL、内部域名或完整租户标识。 - 把网关的超时伪装成业务错误,客户端因此错误重试或错误提示。
错误表现与修正
如果客户端无法判断错误是否可重试,通常是状态码、类型和重试建议没有形成一致契约。修正时先建立类型注册表,再让各层映射到有限的公开类型;对未知类型提供安全默认行为,并用日志关联 ID 追踪内部原因。
生产化实现
在共享库或边缘适配层集中序列化 Problem Details,保留服务边界上的状态码选择。通过 schema 校验限制扩展字段长度、数组数量和 URI 格式;将敏感字段过滤放在序列化前,而非只依赖网关。对 429、503 和网络超时分别定义指数退避、抖动和幂等条件,避免重试风暴。
验证清单
契约测试覆盖每个公开 type 的状态码、媒体类型、必需字段和扩展字段;集成测试穿过网关、服务和队列,断言关联 ID 与脱敏结果;兼容测试用旧客户端验证未知字段和保留 code;压测观察错误序列化延迟、日志采样和限流下的重试放大。
追问及应对
为什么不能只定义一个业务错误码?
单一错误码无法表达 HTTP 缓存、认证、限流和重试语义。保留状态码能让通用基础设施正确工作,type 再承载稳定的业务类别,两者职责不同。
type URI 必须可以访问吗?
规范允许相对或绝对 URI;团队应选择稳定、可文档化的形式。若提供文档页,应避免把页面可用性作为客户端处理错误的前提。
如何避免错误体本身造成信息泄露?
建立公开字段白名单、长度限制和敏感模式扫描;将内部异常映射为通用类型,并在日志中保留关联 ID。安全测试要覆盖跨租户查询、权限失败和异常堆栈路径。
评分标准
- 语义准确:能区分 HTTP 状态码与 Problem Details 字段。
- 契约设计:给出稳定
type、扩展字段和版本策略。 - 边界意识:覆盖网关、异步任务、脱敏和重试风暴。
- 实现可落地:有注册表、序列化边界、兼容矩阵和测试。
- 风险控制:不泄露内部细节,也不把未知类型当作可重试。
合规检查
确认状态码、类型字段、脱敏策略和重试边界在回答中保持一致。
面试作答要点
先说状态码仍表达 HTTP 语义,再说明 type 是机器稳定标识、detail 不是契约键;随后补充网关边界、字段校验、重试、脱敏和兼容验证。
一句话总结
统一错误的关键是让状态码负责协议语义、type 负责稳定分类、扩展字段负责可行动细节,并用安全和兼容测试守住边界。