为什么大厂弃用PUT和DELETE?HTTP方法迁移到POST的工程实践

发布时间:2026/10/8 2:52:23
为什么大厂弃用PUT和DELETE?HTTP方法迁移到POST的工程实践 前几天测试同事在群里甩了个JMeter的报错截图jmeter: could not delete existing file c:\windows\system32\result.jtl。乍一看挺吓人——管理员权限跑JMeter输出文件的相对路径被解析到了系统目录结果连清理临时文件都失败。大家笑骂几句就散了我却借这个由头想起这几年被反复问到的一个问题为什么越来越多的大厂对外API和内部服务都在逐渐弃用PUT、DELETE请求按REST教科书GET、POST、PUT、DELETE是四个主角缺一个都不完整。HTTP语义里PUT负责幂等的整体替换DELETE负责幂等的删除设计得很漂亮。可真到了工程现场你会发现大量接口的POST占比高得吓人有些团队甚至明文规定只允许GET和POSTPUT和DELETE要么被塞进兼容层要么干脆从新接口里消失。这不是审美偏好问题而是被基建、语义、架构层层逼出来的理性选择。这篇文章不打算做REST已死之类的标题党而是老老实实拆解PUT和DELETE在规范里承诺了什么工程现实为什么兑现不了大厂转向POST为主的架构是怎么考虑的以及真要迁移该怎么落地。1. 先看清楚PUT和DELETE在规范里到底承诺了什么1.1 四个方法的官方人设RFC 7231把HTTP方法语义定义得很清晰先列一张表把关键差异摆出来方法是否安全是否幂等规范语义典型用途GET安全幂等读取资源不改变服务端状态查询、拉取POST不安全非幂等在服务器上创建资源或触发动作创建、提交命令PUT不安全幂等用请求体整体替换目标资源完整更新DELETE不安全幂等删除目标资源删除安全在这里不是指传输安全而是这个请求不应该改变服务端状态所以爬虫、预取、浏览器刷新可以放心地重复GET。幂等则是执行一次和执行多次最终结果一致。这两个性质是HTTP协议给上层应用的重要承诺也是网络重试机制的安全垫。做后端的朋友应该都有过这种经历请求超时了客户端自动重试如果用的方法不幂等重试一次就多创建一条数据订单重复、支付重复扣款都是这么来的。PUT和DELETE被设计成幂等就是为了给这类场景一个兜底。1.2 教科书场景为什么设计得这么合理想象一个经典的文件资源接口PUT /files/{id}表示客户端知道ID也知道完整内容整体覆盖这个文件DELETE /files/{id}表示把整个文件从存储里摘掉。因为PUT幂等客户端发请求超时了可以放心重试哪怕第一次其实已经成功了重试一次得到的是同样的结果不会把文件内容改坏。DELETE同理删不存在的资源返回404再删一次还是404状态没有二次破坏。这就是规范设计者的初衷在网络不可靠的假设下用方法的幂等性给重试机制一个安全出口。理论上这套语义放到任何资源型系统里都成立。早期Web API确实大量这么用对象存储、知识库、简单的CMS内容管理基本都是这个套路。我早年在云存储团队时内部的文件接口就是标准PUT/DELETE客户端重试得也很放心因为语义确实对得上。1.3 理论好看落地就走样但理想和现实之间隔着一层工程灰度。我做了十几年后端见过的PUT和DELETE翻车案例比想象中多得多问题几乎都出在同一个地方资源这个抽象概念在真实业务里从来不是一块可以随意整存整取的硬盘文件。举一个最常见的例子PUT /user/123本意是整体替换用户信息但前端只改了手机号把整个对象传了过来——恰好另一个字段用户来源渠道在页面上压根没渲染于是被空值覆盖。等数据对不上账的时候谁都不知道是哪个客户端用哪个版本悄悄把它抹掉的。这不是HTTP的错也不是PUT的错但它让标准语义在真实协作里变得极其脆弱。DELETE的问题更明显业务层面的删除几乎没有一次是真正无条件的。删订单要校验状态、删用户要做合规留存、删文件可能还有多副本一致性——这些逻辑塞不进一个裸的DELETE。于是大量团队最后都走向了POST 动作子资源的方案。说白了规范给的语义太干净了干净到接不住真实业务的脏活。2. 基建层根本不认识REST它只认识流量2.1 企业代理、防火墙和历史遗留405第一座大山是链路中间的代理和防火墙。在不少企业网络里出口代理只放行GET和POSTPUT、DELETE、PATCH这些非主流方法会被直接拦掉。你可以在自己电脑上curl得通但一到客户现场、合作伙伴机房、或者某些安全策略严格的办公网络请求就死在半路了。排查起来极其痛苦因为不是每次请求都走同一条链路过同一个代理时好时坏最难查。更典型的还有老一代Web服务器和应用服务器。早期IIS的WebDAV配置、某些Java应用服务器的Servlet过滤链默认对PUT和DELETE返回405 Method Not Allowed。很多团队第一次踩这个坑是在接手一个老项目时发现线上接口文档写着支持DELETE但生产环境一调就是405最后查出来是运维在Nginx层把WebDAV相关方法全禁了。你说冤不冤接口写得再标准一个中间节点的历史配置就能让整个功能瘫痪。WAFWeb应用防火墙对方法的敏感度也常被忽略。不少WAF的默认规则集里会把异常方法直接标记为攻击流量或者要求所有非GET请求必须带CSRF Token。DELETE这种看起来极具破坏性的方法经常成为规则的热点对象。你可以说规则写得蠢但现实就是你的接口规范再标准也得先过这些法外之地。在全球化的业务里尤其如此不同地区的安全策略不一样你没法控制每个中间节点怎么理解HTTP语义。2.2 CDN和API网关方法决定待遇到了CDN和七层网关这一层问题从安全变成了能力不对称。CDN的缓存体系几乎只认真对待GET只有GET响应可以被缓存PUT和DELETE天然进不了缓存体系。按理说这不影响功能但很多CDN厂商的规则引擎、回源策略、日志分析都是按GET/POST差异化配置的PUT和DELETE在里面是二等公民。你给接口配回源超时、配缓存规则、配鉴权头透传都会发现文档里关于PUT和DELETE的说明含糊不清甚至干脆不支持。API网关那边也类似。网关要给你做鉴权、限流、路由、灰度、审计很多产品把Method作为路由和策略匹配的维度配置面瞬间扩大。比如你要给某个DELETE接口单独配IP白名单就得在网关规则里多写一条完整的方法匹配。团队规模一大这类碎片化配置就是事故高发区——规则漏了、匹配错了、环境差异任何一个都够你从下午排查到半夜。我见过不止一个团队最后把网关上的PUT/DELETE全部重写成POST就是为了少维护一半的规则。2.3 浏览器里的CORS预检绕不过去的OPTIONS如果前端在浏览器里用fetch调用跨域接口PUT和DELETE就触发了CORS预检浏览器先发一个OPTIONS请求问服务器你允许我用PUT跨域访问吗服务器得正确返回Access-Control-Allow-Methods浏览器才肯发真正的请求。这一下就把原本一次HTTP请求变成了两次而且多了一个服务器必须正确实现OPTIONS的硬性要求。我见过不少团队后端接口本身没问题但OPTIONS没处理好导致前端跨域调用PUT/DELETE全部失败最后统一改成POST才消停。POST为什么不用预检因为POST配Content-Type: application/x-www-form-urlencoded或multipart/form-data时属于简单请求不触发预检即使你用application/json也只是因为Content-Type不在简单请求列表里才需要预检处理起来比PUT还是省心。换句话说在浏览器这个高频场景里PUT和DELETE天生多一道关卡而这道关卡还经常卡在别人手里。2.4 把这些限制串起来看我把基建的影响总结成一句话**PUT和DELETE是标准语义的请求但链路上每一个中间环节都有权不按标准办事。**代理可以拦、防火墙可以挡、WAF可以报警、CDN可以不支持、浏览器要预检——任何一个环节出问题接口就不可用。而POST是那个大家默认都会放行的最基本方法用它等于选了最宽的路。做接口设计不是写论文你是在跟无数看不懂论文的中间设备打交道。3. 语义误用才是最大的隐藏成本3.1 把PUT当PATCH用数据被覆盖的经典悲剧前面提到的PUT整体替换被当成局部更新用是业界最常见的语义错位。HTTP社区后来专门设计了PATCH来表示局部更新但PATCH的格式又没有一个统一标准有的是JSON Merge Patch有的是JSON Patch有的干脆是自定义差异格式。于是前端团队索性不学这些了——反正我用POST传JSON最省事。从工程治理角度看PUT带来的风险是静默的。POST更新失败你会看到错误PUT覆盖了不该覆盖的字段你看到的可能是几周后的对账异常。这种不报错的破坏比报错的失败可怕得多。我复盘过好几次线上数据事故最后根因都是某个客户端用PUT提交了不完整对象。说句实在话每次看到团队新接口用了PUT我的第一反应不是标准而是这里会不会埋雷。3.2 DELETE的不可逆和业务删除的复杂性HTTP的DELETE语义很纯粹把这个URI标识的资源摘掉。但业务删除从来不是这么简单的操作数据合规要求删除或匿名化但实际操作要审批、留痕、异步执行订单有状态机只有待支付能删其他状态要先走退款流程被删除的用户名要留档防止被重新注册冒用删除操作往往级联触发一堆下游事件清缓存、发通知、更新统计。这些逻辑如果硬塞进DELETE接口就得靠请求头、请求参数、状态码来补充语义结果是接口文档越写越长每个团队理解的DELETE都不一样。最后大家达成共识与其用一个不得不加各种附加条件的DELETE不如用POST /users/{id}/deactivate这种动作型接口把做什么写在明面上把怎么做写进后端逻辑。3.3 幂等性承诺在实际业务里很难兑现规范说PUT和DELETE是幂等的但规范管不了你的业务状态。同一个DELETE请求第一次执行时资源存在删除成功第二次执行时资源已经被下游异步任务删了返回404——从HTTP语义看两次结果都是删除后的状态算幂等但从业务日志看第一次和第二次走了完全不同的分支可能触发了不同的补偿逻辑。还有个更微妙的问题幂等不等于没有副作用。重试一个DELETE请求即使资源已经没了审计系统、消息队列、监控指标可能每次都照常记录。如果你的账户和计费系统按请求数计费那幂等接口的重试照样花钱。POST反而把问题摊开了规范明确说POST可以非幂等所以你做重试保护是业务自己负责没人拿规范来压你。很多大厂内部为此引入了Idempotency-Key请求头把幂等控制权从HTTP方法转移到显式字段上反而比靠方法语义更可控。这里有个反直觉的点放弃方法自带的幂等换成显式声明的幂等看起来是退步实际上是把模糊的承诺变成了明确的约定。3.4 一个状态机例子DELETE表达不了有条件删除拿订单系统举例。假设业务规则是待支付订单可以删除已支付订单要删除前必须先退款已完成订单只能走售后期流程不能删。用REST方法设计会出现什么你可能做出DELETE /orders/{id}然后在后端里塞满if判断状态不对就返回409 Conflict。接口语义越来越胖谁能删、删了干什么、删不了怎么办全靠一份没人读得懂的错误码文档来约定。而改成POST /orders/{id}/cancel之后取消这个动作包含状态校验、退款发起、通知下游一个动作一个职责后端逻辑清晰前端调用意图也一目了然。这不是说DELETE不能用而是说删除这个动作在真实业务里几乎总是带有条件的而方法语义表达不了条件。状态机越复杂越能看出动作型接口的优势。4. 大厂转向POST一切的真实架构逻辑4.1 GraphQL和gRPC方法被收编进请求体先看两个影响深远的协议设计。GraphQL的规范很明确查询和变更都走同一个端点绝大多数实现用POST提交包含query字段的JSON具体操作是什么、操作哪个资源全在Body里声明。调用方不再需要关心用PUT还是PATCH去改一个字段而是声明我要执行哪个mutation。gRPC更彻底它在HTTP/2之上把整个HTTP方法收敛成了POST。客户端调用order_service.CancelOrder网络层看到的就是对/order_service/CancelOrder这个路径的POST请求。服务方法名、入参、出参都由IDL接口定义语言在编译期约定了REST风格的那套方法语义、URL规范、状态码体系全部不需要了。这不是某一家公司的奇思妙想而是无数团队在大型复杂系统里用脚投出来的票**当接口数量和团队规模上去之后让协议更窄、更确定比保留HTTP方法的各种微妙语义更省心。**方法语义被下沉为Body里的一个字段或IDL里的一个方法名反而更容易做校验、文档生成、代码生成和自动化测试。你仔细想想GraphQL和gRPC壮大的这几年恰好是业界对REST落地质量失望的这几年。4.2 BFF层和内部中间件URL和方法正在退居二线现在的大型互联网系统前端一般不会直接打后端核心服务中间会隔一层BFFBackend for Frontend。BFF的职责是把下游的领域能力聚合、剪裁成前端好用的接口。这种聚合接口天然是动词导向的前端需要一个给我展示订单概要的能力BFF就去调下游多个服务拼数据。动词导向的接口最自然的表达就是POST因为它在做聚合动作而不是操作某个单一资源。数据平台、内部工具、低代码平台那边更明显。你去看很多内部系统的接口规范methodaction模式远多于HTTP方法资源路径模式。比如POST /api/v1/data/fetch、POST /api/v1/task/execute里面的fetch、execute都是动作。整个系统对HTTP方法的区分度很低因为团队早就把请求干了什么放进了路径和Body里。这个趋势在微服务化之后尤其明显——服务之间调用的接口本质上是远程方法调用POST是最接近函数调用的HTTP表达。4.3 安全、审计、风控的统一入口需求到了大厂层面安全风控决定了接口风格。所有外部请求要先过统一的防刷、风控、审计链路这些系统关注的核心字段往往是谁身份做了什么操作操作码对什么资源资源ID带了什么参数Body你发现没有这里几乎没有HTTP方法的戏份。风控系统要识别一个删除用户的操作更愿意统一看opdelete_user这类操作码而不是分散去适配DELETE /users/{id}、POST /users/{id}/delete、PATCH /users/{id}把状态改成deleted这三种风格。入口处的方法多样性意味着风控规则、审计日志、报警策略都要为每一种风格写一遍适配。安全团队不会因为你这个DELETE写法很REST就高抬贵手他们要的是规则可枚举、行为可预测。所以很多大厂内部干脆立规矩外部接口统一GET/POST动作统一写进路径或Body。为了让规则简单可实施砍掉PUT和DELETE是成本最低的方案。这不是技术上的投降是治理上的务实。4.4 风格统一比语义纯正更重要我见过不少技术负责人最开始都是REST原教旨主义者最后都变成了务实派。原因很简单在一个几百人、几千人协作的代码库里**接口风格的一致性本身就是一种基础设施。**如果A团队用DELETE表示删除B团队用POST delete动作C团队用PATCH改状态字段前端SDK、网关配置、风控规则、监控面板、测试框架全都要为三种风格维护三份逻辑。相比之下统一用POST损失一点语义纯度换来的是全局复杂度的大幅下降。Kubernetes是一个值得注意的反例它的API严格遵守PUT整体更新、PATCH局部更新、DELETE删除的语义还能做得很好。但它有完整的类型系统、严格的Schema校验、自动生成的OpenAPI文档、强大的客户端库生态。换句话说K8s能把REST语义撑住是因为它对资源的定义极度严谨并且全员严格按规范协作。大多数人所在团队达不到这种纪律性。所以说PUT和DELETE不是不能用是用得起的条件很苛刻。5. 哪些场景还应该坚持用PUT和DELETE5.1 适合坚持的场景先别急着把所有接口改成POST。PUT和DELETE在一些场景里依然是最优解场景推荐方法理由对象存储、文件资源PUT客户端掌握完整文件内容本来就该整体覆盖公开的纯CRUD资源接口PUT/DELETE资源边界清晰没有业务动作幂等重试友好内部服务间直连无代理/CORSPUT/DELETE链路可控语义明确存量REST API的延续保持现状对已发布的API做方法级破坏是高风险操作需要严格幂等重试的资源操作PUT/DELETE方法自带幂等语义省去额外幂等设计我自己在做对象存储网关时就坚持用PUT做上传覆盖、DELETE做删除因为文件资源太适合这套语义了客户端拿着一整块内容覆盖就是覆盖删就是删没有状态机没有级联动作。这种场景强行改成POST反而是添乱。5.2 一套实际可用的判断清单我在团队里推行过一套选择题遇到新接口先过一遍这个操作是对一个资源的整体覆盖吗客户端能拿到并提交完整数据——是考虑PUT。这个操作是无条件摘掉一个资源吗没有审批、级联、异步回调、合规留存——是考虑DELETE。操作本身是一个业务动作取消、退款、上架、禁用——直接用POST /资源/{ID}/动作。接口要跨浏览器、跨代理、过CDN、经过WAF——默认用GET/POST最稳。团队内部有没有统一的方法规范没有的话优先服从团队的统一约定而不是个人审美。这套清单基本把方法论落到了操作层面而不是停留在REST好还是不好的哲学争论上。设计师可以讨论美学工程接口讨论的是可用性和可维护性标准不一样。5.3 一个容易被忽略的例外查询该不该用POST说到POST一切不少朋友会问那复杂查询呢GET的URL长度有限制查询条件一多就放不下是不是用POST我的建议是复杂查询用POST没有问题很多大厂也是这么干的POST /search、POST /api/query只要约定清楚这个POST是幂等的查询动作不要在服务端搞出副作用就行。真正的禁忌是把有副作用的POST查询混进无副作用的POST查询比如查询里偷偷改状态那才是审计黑洞。方法可以统一但行为的边界必须在规范里写死。6. 真要从PUT/DELETE迁到POST体系怎么落地6.1 先盘家底别拍脑袋迁移的第一步不是写代码而是统计存量。把网关或服务层的访问日志拉出来按方法维度看一眼哪些PUT/DELETE接口还有真实流量错误率多少重试次数多不多调用方是谁很多PUT/DELETE接口其实早就没有客户端在用了只是文档还挂着这种直接下线或给个501都比迁移划算。我之前接手过一个老项目文档上列了二十多个DELETE接口结果访问日志一查有真实流量的只有三个剩下全是扫描器打的。为了迁移那三个真接口花了一周把剩下的直接标记废弃运维那边少了一堆安全隐患。先盘家底这件事省下来的功夫远超预期。6.2 新老并存灰度过渡对还要用的接口我的推荐做法是影子迁移新增POST /resources/{id}/delete这类动作接口实现跟旧DELETE完全一样的业务逻辑旧DELETE接口继续保留在新接口稳定运行一段时间后把旧接口改为返回308 Permanent Redirect或直接返回带Deprecation警告的响应头客户端灰度切换先切内部调用方再切外部合作伙伴全程保留旧的请求日志一旦发现问题能快速回滚。这里特别提醒一点**别在新旧接口共存的窗口期悄悄改旧接口行为。**我有一次就吃过亏以为把DELETE从硬删改成软删没问题结果一个老客户的清理脚本跑了一个月把本该删掉的数据全变成了已删除状态最后对账对了两周。行为变更必须跟方法迁移同步做版本管理别夹带私货。6.3 幂等和安全设计要跟着搬家POST不像PUT/DELETE自带幂等保证所以你需要在动作接口里显式做幂等设计。最通用的是Idempotency-Key请求头客户端为每个操作生成唯一Key服务端用Redis等存储记录Key和响应重复提交就返回第一次的结果。这个做法在Stripe等大厂的公开API里已经很成熟本质上就是把方法保证幂等升级为业务自己保证幂等。安全侧也要检查旧DELETE的鉴权规则、WAF放行规则、网关IP白名单都要同步迁移到新的POST路径。别新接口上线了网关规则还只对DELETE生效那就成了裸奔。我见过一个团队迁移后忘了把限流规则搬过来结果新POST接口被刷了一晚上老DELETE接口反而没人理——规则跟着流量走这个道理在迁移时最容易忘。6.4 测试工具和脚本的适配JMeter那个坑迁移还会波及测试工具。JMeter里跑HTTP请求一般就是建一个HTTP Sampler选方法、填路径、写Body。很多老脚本用的PUT/DELETE迁移后全得改成POST同时把原来写在URL参数里的东西移到JSON Body里。这里有个大坑**JMeter在做结果文件清理时如果以管理员身份启动并且输出文件配置成了相对路径就可能出现could not delete existing file c:\windows\system32\xxx.jtl这类报错。**本质是JVM的user.dir被解析到了系统目录清理逻辑拿着相对路径去system32下删文件权限不够就被拒了。解决办法也很简单JMeter安装和运行都放到专用目录结果文件名配绝对路径别用管理员权限跑。这类问题看着跟HTTP方法没关系但它提醒我们**任何涉及删除的操作包括测试工具的清理、脚本的清理逻辑在工程里都是最容易被环境和权限坑到的地方。**迁移接口时把测试脚本、清理任务、CI里的旧方法调用一起列进改动清单别只改生产代码。我每次做接口迁移都会顺手把团队里的Postman集合、JMeter脚本、curl脚本全都过一遍漏掉一个回归测试就白跑。6.5 迁移之后的观测调整最后别忘了观测体系。原来监控面板按GET/POST/PUT/DELETE统计流量迁到POST为主之后监控维度要改成路径 动作否则你只能看到POST请求暴涨不知道是哪个业务动作在涨。日志索引、告警阈值、报表口径都要同步调整这一步看似琐碎但做不好下次事故排查时你会发现连哪个接口在被谁调用都说不清楚。我倾向于在迁移完成后的一个月内每周拉一次方法分布和动作分布的双维度报表确认旧方法流量确实降到接近零再走正式下线评审。很多团队死在最后一步——旧接口没有正式下线留着留着就成了安全隐患扫描器一打一个准。该关的接口就要果断关别让僵尸接口一直躺在你的API列表里。我个人的体会是PUT和DELETE被逐渐边缘化不是HTTP协议设计失败了而是工程世界用脚投票的结果——在大规模协作里明确、一致、好实现比语义优雅更重要。POST不是万能银弹但它是所有方法里链路兼容性最好的那个也是一个团队能把规则定得最死的那个。与其纠结标准不标准不如务实一点资源边界清晰、链路可控、团队纪律严格就放心用PUT和DELETE要过各种代理网关、要跨团队协作、要统一风控审计就老实走POST 动作。选择方法之前先想清楚你的请求要经过哪些不讲理的环节这比翻十遍REST规范都管用。