向后兼容规格:如何规划废弃路径与破坏性变更

向后兼容规格:如何规划废弃路径与破坏性变更
Daniel Marsh · Spec-First 工程笔记

你在周五下午发布了 API 的新版本。到周一早上,三个下游团队已经提交了故障工单,因为他们的集成全部崩溃了。他们依赖的字段被重命名了,一个枚举新增了一个值但他们的 switch 语句没有 default 分支,一个之前返回 null 的字段现在返回空字符串。这些变更在代码 diff 中没有一个看起来明显危险。这就是破坏性变更的问题所在:伤害最大的往往是那些没人认为会造成破坏的变更。

发布于 2026-03-27 · ✓ 已更新 2026-05-11 · 阅读约 7 分钟 · 作者:Daniel Marsh · 审校:编辑政策

什么样的变更算"破坏性"

破坏性变更是指任何导致现有消费方在不修改代码的情况下就会失败的修改。显而易见的情况容易识别:删除端点、重命名字段、更改 URL 路径。但真正让团队栽跟头的是那些不起眼的变更。

给枚举添加一个新值从技术上说是增量变更,但如果消费方将其反序列化为严格类型,或者用一个没有 default 分支的 switch 语句处理它,消费方就会崩溃。把一个字段的返回值从 null 改为空字符串 "" 不算 schema 变更,但它会破坏下游所有的空值检查逻辑。改变现有字段的语义含义,比如让 created_at 返回 UTC 而不是用户本地时区,不改变类型也不改变名称,但它会改变每一个依赖该值的计算结果。

你的规格应该定义什么算作你的 API 的破坏性变更。一个好的起始清单:删除或重命名字段、更改字段类型、修改现有行为的响应状态码、更改错误格式、添加必需的请求参数、收紧对之前接受的输入的验证规则、以及更改任何字段的语义含义。把这个清单写下来。放进你的 API 版本控制规格里。当提议的变更匹配清单中的任何一项时,废弃流程就启动了。

废弃时间线规格

没有时间线的废弃只是一个愿望。消费方需要的是日期,不是模糊的警告。废弃规格是一份简短的文档,告诉每个利益相关方究竟发生了什么、什么时候发生、以及他们需要做什么。

以下是一个在实践中有效的模板:

# 废弃规格:GET /v2/users/:id 响应结构

## 变更内容
`address` 字段(当前为纯字符串)将被替换为结构化的
`address` 对象,包含 `street`、`city`、`state` 和
`postal_code` 字段。

## 时间线
- 2026-05-01:宣布废弃。响应中同时返回 `address`(字符串)
  和 `address_structured`(对象)。
- 2026-06-01:在 /v2/users/:id 响应中添加 Sunset 头部。
  对仍在读取 `address` 的消费方记录废弃警告。
- 2026-08-01:从 /v2/users/:id 中移除 `address` 字段。
  仅保留 `address_structured`(重命名为 `address`)。

## 迁移路径
将 `response.address` 替换为 `response.address_structured.street`
(或完整对象)。参见迁移指南:/docs/migrations/user-address-v3.md

## 支持
在 Slack 的 #api-platform 频道联系我们获取迁移帮助。
迁移答疑时间:每周四下午 2-3 点(太平洋时间),持续至 6 月。

关键要素:对变更的具体描述、带有明确日期的时间线、包含代码级指导的迁移路径、以及支持渠道。缺少任何一项都会让消费方只能靠猜。时间线应该至少给消费方一个完整的发布周期来完成迁移,最好是两个。对于外部 API,90 天是一个合理的最低期限。对于消费方数量已知且较少的内部 API,如果你主动联系每个团队,30 天也可以。

为消费方制定迁移路径

告诉消费方"这个字段已废弃"不算迁移路径。迁移路径是告诉他们具体要改哪些代码,以及新代码长什么样。如果你在废弃一个字段,展示变更前后的对比。如果你在更换端点,把旧调用和新调用并排展示。

对于上面的 address 示例,迁移指南应该包含类似这样的内容:

# 变更前(v2,已废弃)
user = api.get("/v2/users/123")
street = user["address"]  # "742 Evergreen Terrace, Springfield, IL 62704"

# 变更后(v3)
user = api.get("/v2/users/123")
street = user["address_structured"]["street"]  # "742 Evergreen Terrace"
city   = user["address_structured"]["city"]     # "Springfield"
state  = user["address_structured"]["state"]    # "IL"
zip    = user["address_structured"]["postal_code"]  # "62704"

好的迁移指南还要覆盖边界情况。如果用户没有设置地址,address_structured 为 null 怎么办?没有 state 字段的国际地址怎么处理?在两个字段同时存在的过渡期内,消费方该怎么做?在指南中回答这些问题,而不是在废弃通知发出三周后在 Slack 里回答。

如果迁移需要消费方重新认证、更新 SDK 版本、或者改变基础设施(比如从轮询切换到 Webhook),明确说出来。目标是让开发者读完迁移指南就能完成迁移,不需要再问任何人任何问题。

为废弃本身做版本控制

废弃不仅仅是变更日志里的一条注释。它应该是机器可读的,这样监控系统、客户端库和 CI 管道就能自动对其做出反应。有两个标准被广泛使用:Sunset HTTP 头部和 Deprecation HTTP 头部。

Sunset 头部(RFC 8594)告诉客户端某个资源何时将不再可用。它使用标准的 HTTP 日期格式:

HTTP/1.1 200 OK
Sunset: Sat, 01 Aug 2026 00:00:00 GMT
Deprecation: Mon, 01 Jun 2026 00:00:00 GMT
Link: <https://api.example.com/docs/migrations/user-address-v3>; rel="sunset"

当消费方看到 Sunset 头部时,他们的 HTTP 客户端或中间件可以记录警告、触发告警,甚至阻止依赖即将下线端点的部署。带有 rel="sunset"Link 头部指向迁移文档。这把废弃从一种纯人工沟通变成了工具可以强制执行的东西。

在你的 API 规格中,定义你将使用哪些头部、何时添加它们、以及消费方的预期行为应该是什么。契约测试可以验证已废弃的端点是否返回正确的 Sunset 头部,以及日期是否与废弃规格中的时间线一致。如果头部显示 8 月 1 日但规格写的是 7 月 1 日,说明有人搞错了。契约测试能捕获这种错误。

沟通规格:变更日志与通知

有废弃规格是必要的,但还不够。你还需要把信息推送到需要它的人那里。变更日志里一条"废弃了 address 字段"的记录,无法触达那个六个月前部署了集成并且再也没查看过变更日志的团队。

你的沟通规格应该定义三件事:沟通什么、什么时候沟通、通过什么渠道沟通。

沟通什么:每条废弃通知都应该包含变更摘要、时间线、迁移指南链接和支持渠道链接。不要让消费方到处找细节。一条通知,所有信息,每次都是。

什么时候:在宣布时通知,在 Sunset 头部生效时再次通知,在下线日期前 14 天做最后提醒。对于外部 API,增加一个 30 天提醒。三个触达节点是最低要求。如果你要移除的东西有几十个团队依赖,就增加更多。

渠道:变更日志、API 状态页面、发送给已注册 API 消费方的邮件、给已知消费团队的 Slack 或 Teams 消息、以及响应中的废弃头部。仅存在于变更日志中的废弃通知,是大多数消费方会错过的废弃通知。去消费方已经在的地方找他们。

追踪确认情况。如果一个消费团队在 14 天内没有确认收到废弃通知,直接跟进。不要假设沉默意味着他们已经看到并在处理了。沉默通常意味着他们根本没看到。

什么时候破坏性变更才是正确选择

不是每个变更都值得走温和的废弃路径。有些变更需要快速执行,90 天的时间线造成的损害会比直接破坏更大。

安全漏洞。如果一个端点泄露了个人身份信息或存在认证绕过,立刻修复。事后再通知消费方。为安全漏洞制定废弃时间线就是为攻击者制定利用时间线。先修复,再沟通变更,再帮助消费方适应。按这个顺序来。

合规要求。当法规要求你在特定日期前停止返回某些数据时,法律决定时间线,而不是你的废弃政策。GDPR 删除权、PCI 范围缩减、数据驻留要求:这些优先于你的标准流程。在废弃规格中注明法规驱动因素,让消费方理解为什么时间线被压缩了。

不可持续的维护负担。如果维护与某个遗留端点的向后兼容性需要你的团队保持一个已废弃的数据库运行、维护两种序列化格式、并且每次发布都要针对一个与系统实际数据模型不再匹配的契约进行测试,那么兼容性税可能超过了迁移成本。在这种情况下,较短的废弃窗口配合积极的迁移支持,胜过无限期的维护承诺。

关键是对权衡保持坦诚。当你快速破坏兼容性时,承认你给消费方带来的代价。提供专门的迁移支持。主动和受影响的团队结对。破坏的速度与你应该提供的支持力度成反比。

事后分析:没有废弃规格会怎样

一次没有契约测试的破坏性 API 变更的事后分析记录了当一个团队在没有废弃流程的情况下发布破坏性变更时会发生什么。响应结构变了,没有通知消费方,没有迁移路径,三个服务在生产环境中故障了。

事后分析将根本原因归结为缺失的兼容性契约。该团队没有规格来定义什么构成其 API 的破坏性变更。没有这个定义,做出变更的工程师真心认为这不是破坏性的。他们把一个字段从 user_name 重命名为 username,以符合团队的命名规范。从他们的角度看,这是一次清理。从消费方的角度看,这是一次故障。

补救措施包括采用向后兼容规格、在 CI 中添加契约测试、以及实施废弃检查清单。所有这些都是本可以在事故发生前完成的事情。在生产故障之后才构建废弃流程的成本永远高于在之前构建的成本。你得付出事故响应的开销、与消费团队修复信任关系的代价、以及在压力下仓促实施的成本。

如果你正在阅读这篇文章是因为想避免这类事故,起点很直接。定义什么对你的 API 算破坏性变更。写下来。在你的规格模板中添加废弃时间线。接入 Sunset 头部。在 CI 中运行契约测试。在需要它之前就制定好上线与回滚计划,而不是在事故期间。

关键词:向后兼容 · API 废弃 · 破坏性变更 · 废弃时间线 · Sunset 头部 · API 版本控制 · 迁移路径 · 契约测试

专题阅读路径

这篇文章归入 API 契约 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。

交互式生成规格
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
试用规格生成器

编辑说明

本文面向 API 驱动的软件团队,介绍向后兼容与废弃规划。示例均为工程场景说明,不构成法律、税务或投资建议。

本页合并覆盖的主题

为了让文章库更聚焦,这篇主文章现在作为「向后兼容规格:如何规划废弃路径与破坏性变更」的规范入口,同时覆盖下面这些原本分散的相关主题。读者可以在一个页面里完成判断、复制和评审,不必在多篇相似文章之间来回跳转。

  • API 废弃计划:写进契约里的兼容通知
  • API 契约版本控制策略
  • 向后兼容的 API 变更规格写法