现象
一次例行代码走查中发现:订阅接口的 catch 分支在数据库写入失败时返回 state: 'invalid' + HTTP 503 + message: '订阅服务暂时不可用,请稍后重试。'。而前端只按 state 映射两种文案:rate-limited → “尝试次数过多”,其余一律 → “邮箱格式似乎不对,请检查后重新提交。”——服务端精心准备的 message 被整块丢弃。
后果:服务真挂了的时候,用户反复修改正确的邮箱地址,越改越困惑;“稍后再试”的引导完全没机会触达。
根因
两层各自的问题叠在一起:
- 服务端状态复用:
invalid本意是“输入无效”,故障分支图省事也塞进了invalid,真实原因只活在没人读的message字段里。状态枚举['pending','confirmed','invalid',...]里没有一个表达“服务暂不可用”的取值。 - 前端把映射写死成二元:
state === 'rate-limited' ? A : B,隐含假设“失败必是输入问题”。
修复
- 前端:
payload.message存在时优先透传,否则按response.status兜底分桶(429 限流文案、≥500 服务不可用文案、其余输入文案)。 - 状态页:当服务端通过 redirect 带上
message查询参数时同样优先展示,保证无 JS 的原生表单流也看到真实原因。 - 服务端(遗留建议):为服务故障引入独立的
unavailable状态值属于契约变更,按变更控制流程登记后再做。
教训
- 面向用户的文案要透传,不要在客户端重新发明。 服务端最清楚失败原因,前端的职责是渲染而非猜测。
- 错误状态枚举要能区分“用户可修复”与“用户不可修复”两类,前者引导改输入,后者引导等待或换渠道(RSS)。
- 走查这种问题最有效的手段:把接口的每一个失败分支在本地用 mock 打出来,对照前端实际显示——本项目随后用 Playwright 路由拦截把“503 透传”固化成了回归用例。