一大早还没打开咖啡,就被线上群里的截图砸醒:前端同事在喊“你把删除接口改成 DELETE 之后,浏览器全红了”。我看了一眼 Network 面板,请求方法赫然是 DELETE,返回 405 Method Not Allowed。我第一反应是网关白名单没放开,果然,安全策略一直只放行了 GET 和 POST,业务侧一用“冷门方法”就直接被拦。
这类问题我见过太多次。HTTP 的所谓 9 个请求方法——GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH,看起来是面试背答案的基础题,但真正落到项目里,它能牵扯出网关策略、缓存语义、CORS 预检、CSRF 防护、接口幂等设计等一堆连锁问题。这篇文章我想把这 9 个方法逐个拆开,结合日常开发里真正会遇到的报错场景来讲,比如 400、403、405、502,以及 PUT 和 PATCH 混用导致的数据覆盖事故。适合正在做前后端开发、写 API 接口,或者准备面试想系统梳理一遍 HTTP 基础的人。
1. 为什么“9 个请求方法”是前后端共同的必修课
1.1 请求方法不是面试题,而是 API 的“动词”
我习惯把一次 HTTP 请求类比成一次快递配送。URL 是收货地址,请求头是包裹上的备注信息,请求体是箱子里装的东西,而请求方法就是你要快递员“做什么”的指令——是查询包裹到哪了、投递新包裹、还是把送错的件退回去。
REST 风格接口的核心就是把“方法 + 资源”组合成操作语义:GET /users/1 是查用户,PUT /users/1 是完整更新用户,DELETE /users/1 是删除用户。这个语义看起来简单,但它的价值远不止“接口好看”。网关、缓存、日志审计、安全策略这些基础设施,全都是基于方法做默认行为的。比如 HTTP 缓存规范里默认只缓存 GET 请求的响应;WAF 和 API 网关通常按方法做白名单控制;访问日志里如果看不到方法字段,出问题时你根本还原不了当时的操作路径。
很多团队开发时图省事,所有接口一律 POST,理由是“GET 传参有限制,POST 想传什么传什么”。短期跑得通,但等到要做缓存、做限流、做权限审计的时候就会发现,所有请求都是一个方法,基础设施没办法按语义区分操作类型,只能靠解析 URL 路径去猜,维护成本直接翻倍。
1.2 方法语义理解不到位,排查问题会走弯路
更现实的问题是,方法用错导致的报错非常隐蔽。热词搜索里能看到大量真实开发者的求助,比如常见的 “400 Bad Request: The plain HTTP request was sent to HTTPS port”,这个其实不是方法的问题,是客户端用明文 HTTP 访问了 HTTPS 端口;“HTTP Basic: Access denied” 多半是认证凭据错了;而 “405 Method Not Allowed” 才是真正意义上的方法不被允许。
但很多人排查时第一反应是去改代码逻辑,而不是先看方法语义对不对。我见过有人为了绕开 DELETE 被网关拦截的问题,硬是把删除接口改成 GET 加参数,结果爬虫和预加载机制把线上数据删掉了一大批。这就是典型的为了眼前报错牺牲方法语义,最终被 HTTP 缓存和浏览器行为反噬。理解这 9 个方法的边界,能帮你把“排查范围”缩小一大半:方法相关的报错看方法和路由,格式相关的看报文内容,认证相关的看 Authorization 头,服务器内部错误再看应用日志。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一次 HTTP 请求里,方法放在哪、由谁决定
2.1 报文结构:方法永远写在请求行第一位
先看一个真实的 HTTP 请求报文,这是理解一切的基础:
http复制POST /api/v1/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer xxxxx
{"name": "tom", "age": 18}
第一行叫请求行,由三部分组成:请求方法、请求目标、协议版本。也就是说,服务器收到请求后第一眼看到的就是方法。这一行的格式是固定的,方法名是大小写敏感的,按规范必须全大写。
服务端拿到请求行之后,会根据方法加路由来做分发。比如 Express 这种框架里,同样是 /users 路径,不同方法可以对应完全不同的处理函数:
javascript复制app.get('/users', listUsers); // 查询用户列表
app.post('/users', createUser); // 创建用户
app.put('/users/:id', replaceUser); // 整体替换用户
app.patch('/users/:id', updateUser); // 部分更新用户
app.delete('/users/:id', deleteUser); // 删除用户
这里有一个很多人踩过的坑:如果客户端用了一个路由里没有的方法,比如框架里只定义了 GET /users,你发一个 PUT /users,标准语义是服务器应该返回 405,并且在响应头里带 Allow 字段告诉客户端“这个资源支持哪些方法”。但实际很多框架会直接返回 404,因为路由匹配不到就按“资源不存在”处理了。所以看到 404 别急着改 URL,先确认方法是不是匹配。
2.2 方法用错时,状态码会给你什么信号
状态码不是方法,但它是判断方法用没用的最直接信号。下面这张表是我在实际排查中整理的高频组合:
| 状态码 | 含义 | 与方法相关的典型场景 |
|---|---|---|
| 200 OK | 成功 | 任意方法处理成功 |
| 201 Created | 创建成功 | POST 或 PUT 创建新资源后 |
| 204 No Content | 成功但无返回体 | DELETE 成功后常用 |
| 400 Bad Request | 请求格式不对 | 方法对,但请求头或请求体不符合要求 |
| 401 Unauthorized | 未认证 | 需要登录态或 token |
| 403 Forbidden | 已认证但无权限 | 方法被安全策略主动拒绝 |
| 404 Not Found | 资源不存在 | 路径错,或某些框架方法不匹配时也返回它 |
| 405 Method Not Allowed | 方法不允许 | 路由存在但不支持当前方法 |
| 408 Request Timeout | 请求超时 | 上传大 body 时容易遇到 |
| 413 Payload Too Large | 请求体太大 | POST/PUT/PATCH 上传大文件超过服务器限制 |
| 500 Internal Server Error | 服务端内部错误 | 与方法无关,查应用日志 |
| 502 Bad Gateway | 网关收到了上游无效响应 | 常见于反向代理后端崩溃或超时 |
| 504 Gateway Timeout | 网关超时 | 上游处理太久 |
区分 405 和 403 在排查时很关键。405 是服务器在说“我知道这个资源,但不接受你这个方法”,通常改方法就行;403 是服务器在说“我认识这个方法,但你没权限用”,常见于网关白名单拦截,此时你得去改安全策略或换一个被允许的方法。
2.3 方法不仅是技术选择,也是攻击面的一部分
很多 Web 安全练习和 CTF 题目会专门考请求方法,比如“极客大挑战 2019”这类题目里,就有通过修改请求方法绕过限制的考点。原理是部分开发者在写权限校验时只处理了 POST,比如只对 POST 请求校验 CSRF token,却忽略了 PUT、DELETE 这些方法同样能修改数据,于是攻击者把方法换成 PUT 就直接绕过了校验。
这也是为什么我前面强调基础设施一定要基于方法做统一管控。在反代或网关层,如果业务不需要 TRACE、CONNECT,就应该直接禁用;不需要 PUT/DELETE 的路径也不要放开。方法本身没有好坏,但“所有方法一视同仁”的配置方式,等于把攻击面完全交给了应用代码,一旦代码里有疏漏,基础设施兜不住。
3. 高频三件套:GET、HEAD、POST 的实战边界
3.1 GET:幂等、可缓存,但 URL 别放敏感数据
GET 是最常用的方法,语义是“获取资源”。它有两条核心属性:安全且幂等。安全是指它不应该改变服务器状态;幂等是指同一个请求执行一次和执行一百次,结果是一样的。这两条属性决定了 GET 可以被浏览器、CDN、反向代理做缓存。
实际开发里我用 GET 时的几个习惯:
第一,查询参数放进 query string 或路径参数都可以,但要避免把密码、token、身份证号这类敏感信息放进去。很多 Nginx 默认配置会把完整请求行写进 access.log,URL 里的参数是纯文本明文记录的,一旦日志泄露,等于把密钥直接交出去了。
第二,不要依赖“GET 没有请求体”这一点就无限堆 URL 长度。规范里没有限制 URL 长度,但浏览器和服务器都有隐性限制。Nginx 默认的 large_client_header_buffers 是 4 个 8k 缓冲区,超过就会返回 414 Request-URI Too Large。所以超长查询条件、批量查询 ID 列表这类需求,规范做法是设计成 POST 请求,把条件放到 body 里。
第三,不要在 GET 接口里做有副作用的操作。有人喜欢用 GET 触发邮件发送、触发状态变更,想着浏览器直接打开链接就能触发,很方便。但缓存、预取、爬虫都会导致这个请求被执行多次,副作用会被无限放大。我经历过一次线上事故,运营把“发送通知”做成了 GET 接口,结果内部监控系统周期性探测 URL 存活,每次探测都触发一次真实通知发送,用户被短信轰炸了一下午。
3.2 HEAD:想看“有没有”和“多大”的省流量利器
HEAD 和 GET 几乎是双胞胎,唯一区别是服务器收到 HEAD 请求后,只返回响应头,不返回响应体。它的响应头应该和 GET 保持一致,尤其是 Content-Length,应该表示如果发 GET 请求时响应体有多大。
什么时候用 HEAD?我在项目里用最多的三个场景:
- 探测一个下载文件是否存在、大小是多少。比如前端要下载一个大文件,先用 HEAD 请求拿到 Content-Length,再决定是否发起真实下载,省流量也省带宽。
- 做站点健康检查。监控系统探测 URL,如果只是想确认服务活着,用 HEAD 比 GET 轻量得多。
- 检查资源是否更新。通过 HEAD 响应里的 Last-Modified 和 ETag,可以和本地缓存做对比,避免下载整个文件。
调试时 curl -I 发出去的就是 HEAD 请求,这个命令我几乎天天用。
需要注意,HEAD 请求虽然不返回 body,但服务器端业务逻辑通常还是会完整执行。如果你在 GET 处理函数里做了大量查询和计算,HEAD 也一样慢。我曾排查过一个“HEAD 请求导致数据库连接池被打满”的线上问题,原因是健康检查用 HEAD 批量探测,而应用代码没有区分 GET 和 HEAD,每次 HEAD 都跑了一遍全量报表聚合逻辑,直接把数据库拖垮了。
3.3 POST:创建和提交,重试问题要自己管
POST 的语义是“向指定资源提交数据,让服务器处理”,最常见的用法是创建子资源,比如 POST /articles 创建一篇文章。它和 GET 最大的区别在于:非安全、非幂等,且默认不可缓存。也就是说,同一个 POST 请求重复发送,服务器可能创建出多篇文章。
这个“重复发送”问题在真实环境里太常见了。用户网络抖动导致请求超时,前端自动重试,如果后端没有做幂等处理,就会产生重复订单。我推荐的项目实践是引入幂等键机制:客户端每个 POST 请求生成一个唯一 ID,放在自定义请求头里,比如 Idempotency-Key,服务器在拿到这个 Key 之后先查一下是否处理过,处理过就直接返回上次的结果,不再重复执行业务逻辑。Stripe 的 API 就是这么设计的。虽然幂等键不是 HTTP 标准,但作为团队内部约定非常实用。
另外要提醒一点:POST 配合 301/302 重定向时,很多浏览器和客户端会把 POST 改成 GET,导致请求体丢失。如果后端接口需要对 POST 做临时重定向,要使用 307 Temporary Redirect 或 308 Permanent Redirect,这两个状态码会保留原始方法和请求体。
4. 最容易混淆的 PUT 与 PATCH:整存还是补丁
4.1 PUT 的正确使用姿势:完整替换,可安全重试
PUT 的语义是“用请求里的负载完整替换目标资源”。客户端发送的应该是资源的完整状态,服务器收到后直接用这份数据覆盖原有资源,没提到的字段按默认值处理或者清空。
PUT 最重要的属性是幂等。因为它是全量替换,你发一次和发一百次,最终资源状态完全一样。这个特性让 PUT 很适合在网络不稳定的场景下安全重试。比如客户端要把一篇文章改成某个固定内容,网络超时后直接重发同一次 PUT 请求,不用担心中间状态被破坏。
PUT 也可以用来创建资源,前提是客户端能确定资源的完整 URI。比如 PUT /users/zhangsan,如果 zhangsan 这个用户不存在,服务器创建一个;如果存在,就整体覆盖。这叫“upsert”语义,很多对象存储和配置管理接口都是这么设计的。
4.2 PATCH 是“增量修改”,把变更描述交给服务器
PATCH 由 RFC 5789 定义,语义是“对资源做部分修改”。它和 PUT 的本质区别在于:PUT 发送的是资源的新完整状态,PATCH 发送的是“变更描述”。比如只改用户昵称,PATCH 请求里只需要带新的 nickname 字段,服务器把它合并到现有资源上。
PATCH 的 Content-Type 有好几种规范格式。RFC 6902 定义了 JSON Patch,用数组描述操作序列;RFC 7396 定义了 Merge Patch,用对象描述要合并的字段。实际开发中,大多数团队并没有严格实现这两种格式,而是约定“PATCH 请求体里带哪些字段就更新哪些字段”。这种简化方案能用,但要注意:服务端必须区分“字段没传”和“字段传了 null”,否则无法表达“把某个字段清空”这种操作。
PATCH 不保证幂等。同一个 PATCH 请求发两次,第二次应用时目标字段可能已经不是第一次的基线了。比如客户端发“age 加一岁”,服务端实现成读当前 age 再加一,那么这个 PATCH 重复执行两次,年龄就加了两岁。如果你设计的 PATCH 接口需要支持安全重试,建议配合 If-Match 头或 ETag 做并发控制:客户端先 GET 拿到资源版本号,PATCH 时带上 If-Match,服务器发现版本不匹配就返回 412 Precondition Failed,客户端重新拉取最新数据再重试。
4.3 一次“字段被清空”的线上事故复盘
我在前公司经历过一次非常典型的 PUT/PATCH 误用事故。
当时个人资料页的更新接口是用 PUT /user/profile 实现的,客户端每次更新前会把整个用户对象从服务端拉下来,前端表单修改后,整个对象再 PUT 回去。某次发版后,服务端新增了一个 avatar 头像字段,但 App 客户端没有升级,老版本用户修改昵称时,请求体里根本没有 avatar 字段。按照 PUT 的完整替换语义,服务端把用户头像清空了。
事故影响很大:大量用户头像一夜之间变成默认图。事后复盘,根因就是接口设计时没有严格按语义选择方法。修的时候我们做了两件事:第一,把用户资料更新接口从 PUT 改成 PATCH,只更新请求体里出现的字段;第二,明确了一个团队规范,整体替换场景才用 PUT,接口文档必须标注“缺失字段将按默认值覆盖,调用方必须保证传完整对象”。从那以后,类似的数据覆盖问题再没出现过。
4.4 PUT 和 PATCH 的状态码配合
方法选对了,返回码也要配合好。PUT 成功更新已有资源,返回 200 或 204 都可以;如果 PUT 创建了新资源,应该返回 201 Created;PATCH 成功一般返回 200,响应体里带更新后的完整资源,方便客户端直接刷新页面。如果 PATCH 校验失败,常见的返回是 422 Unprocessable Entity,表示请求体格式能解析但语义不对。
这里还要提一下老接口改造的兼容问题。很多老系统里写的是 POST /user/update 这种 RPC 风格接口,要迁移成 PATCH /users/{id},容易被忽视的是网关层的 Allow 方法和 CORS 预检配置。如果你的前端部署在另一个域名,跨域请求 PATCH 会先触发 OPTIONS 预检,服务器必须在 Access-Control-Allow-Methods 里明确包含 PATCH,否则浏览器会直接拦截,看到的现象就是请求发了但 Network 里全是红色报错。
5. DELETE、OPTIONS、TRACE、CONNECT:冷门方法的价值与坑
5.1 DELETE:幂等删除,注意 CSRF 与批量删除设计
DELETE 的语义是删除指定 URI 的资源,它是幂等的:删除一个不存在的资源,服务器依然可以返回成功,因为最终状态都是“这个资源不存在”。删除成功后,常见的返回是 204 No Content,不带响应体很干净;如果删除后返回 200,可以在响应体里附一句说明。也有团队用 202 Accepted 表示“删除请求已受理,正在异步执行”,适合删除操作很重的场景。
实际项目里 DELETE 有几个容易踩的坑。第一,不要在 <a> 标签的 href 里绑定删除链接,搜索引擎爬虫和浏览器预取可能直接请求这个 URL,导致未授权删除。删除操作必须用表单按钮加 JS 或 fetch 触发。第二,浏览器端用 fetch 发送 DELETE 请求时,如果接口依赖 Cookie 做会话认证,要格外注意 CSRF 防护。很多框架默认只对 POST 做 CSRF 校验,DELETE 容易被遗漏,这就是热词里出现的那种 “no valid crumb” 报错的原因——Jenkins 这类工具会强制要求请求带 CSRF crumb,否则直接 403。
批量删除是另一个常见设计纠结。有人习惯 DELETE /users?id=1,2,3,把 ID 列表放 query 里,但 DELETE 的 body 在部分代理和服务器实现里会被忽略,不可靠。更稳妥的方案是:能在路径里表达的单条删除用 DELETE,非要把批删做成独立接口,可以用 POST /users/batch-delete,接受 ID 数组。虽然这个接口名字带 RPC 味,但它在各种网络环境下行为最一致。
5.2 OPTIONS:既是能力探测,又是 CORS 预检的“隐形流量”
OPTIONS 的官方语义是“探测服务器支持哪些方法”,服务器需要在响应头的 Allow 字段里列出该资源支持的所有方法。手动调试时可以用 curl 看:
bash复制curl -i -X OPTIONS https://api.example.com/users/1
响应里会看到类似 Allow: GET, PUT, PATCH, DELETE, OPTIONS 这样的字段。
不过绝大多数开发者接触 OPTIONS,是因为跨域请求。浏览器规定,跨域的“非简单请求”——比如方法不是 GET/POST,或者带了自定义请求头——会先自动发送一个 OPTIONS 预检请求,问服务器“你允许我这个方法吗?允许带这些头吗?”服务器必须在响应里带上 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers,浏览器确认没问题后,才发真实请求。
这里有个实际建议:后端在处理 OPTIONS 时不要做业务鉴权。因为预检请求本身不携带业务数据,Header 里也不会有 Authorization,如果后端对 OPTIONS 也要求登录态,预检就会失败,浏览器会报告跨域错误,但你排查时看到的真实请求却一切正常,很容易被绕进去。正确做法是让 OPTIONS 请求直接返回 204 和 CORS 响应头,把鉴权留给真实请求。为了减少预检频率,可以在服务器配置 Access-Control-Max-Age,让浏览器在一定时间内复用预检结果。
5.3 TRACE 与 CONNECT:一个回显诊断,一个搭建隧道
TRACE 方法的语义是“服务器把收到的请求原样返回给客户端”。它有点像网络诊断里的 ping,客户端可以通过对比发送内容和返回内容的差异,判断中间有没有代理修改请求。比如某个请求经过代理后多了一个 Via 头,用 TRACE 就能看出来。
但 TRACE 在现代 Web 服务里几乎被全线禁用了。原因是它如果被开启,攻击者可能诱导用户发起 TRACE 请求,借脚本读取 HttpOnly Cookie 等敏感头信息,形成跨站追踪攻击。所以安全基线一般建议在服务器层直接返回 405 或 501。如果你用 curl 发一个 TRACE 请求测试自己的站点,发现返回 405 Method Not Allowed,这是好事,说明方法白名单生效了。
CONNECT 的语义和前面所有方法都不同,它不是为了操作资源,而是建立一个到目标服务器的隧道。最常见的使用场景是 HTTP 代理:客户端通过代理访问 HTTPS 站点时,会先发一个 CONNECT 请求,告诉代理“我要和某个域名:端口建立隧道”,代理同意后,后续流量在隧道里加密传输,代理只负责转发。
生产环境里,CONNECT 通常只允许开放给明确的代理端口,并且很多出口代理会限制 CONNECT 的目标地址和端口,防止内网被当作跳板去探测其他系统。这不是什么高深的技巧,纯粹是基础设施层面的合理管控。
5.4 HTTP 和 HTTPS 下方法有区别吗?顺便聊聊 RPC 的对比
这是一个常被问的问题:HTTPS 加密之后,请求方法是不是也加密了?答案是没有。HTTPS 加密的是传输层内容,但一旦到达服务器的 TLS 终止层,解密后的明文 HTTP 报文里,方法仍然是明文可见的。也就是说,网关联的 WAF 策略想要按方法做拦截,是完全可行的,它不需要解密到应用层,在反代这一层就能看到 GET、POST、PUT 这些动词。这也是为什么很多网关能直接配置“只允许 GET 和 POST,其他全拒绝”。
从 HTTP 和 RPC 的对比来看,能更清楚方法的定位。像 gRPC 这类 RPC 框架,本质上是把 HTTP 当作传输层,业务数据用 Protobuf 编码后放在 body 里,请求方法固定使用 POST,真正的“方法名”是通过请求路径或 header 传递的。HTTP 的请求方法在这个场景里变成了纯粹的传输标记,资源的操作语义完全由 RPC 框架自己约定。这也是为什么很多团队说“REST 和 RPC 不是对立关系”——HTTP 方法表达的是资源操作语义,RPC 方法表达的是函数调用语义,选哪种取决于你的接口更像资源集合还是更像服务函数。
6. 方法选择决策表与网关配置实战
6.1 先想清楚资源语义,再定方法
在动手写接口之前,先问自己三个问题:这个操作是读还是写?写操作是全量覆盖还是增量修改?这个操作重复执行有没有副作用?把答案套进下面这个决策表,基本不会选错:
| 场景 | 推荐方法 | 幂等 | 可缓存 | 说明 |
|---|---|---|---|---|
| 查询单个/列表资源 | GET | 是 | 是 | 参数放 query,不要带敏感数据 |
| 只获取响应头,不取 body | HEAD | 是 | 是 | 适合探测资源存在性和大小 |
| 创建资源,ID 由服务器生成 | POST | 否 | 否 | 配合幂等键防重复提交 |
| 创建或完整替换,客户端指定 URI | PUT | 是 | 否 | 缺失字段会被覆盖,谨慎使用 |
| 只修改资源的部分字段 | PATCH | 不保证 | 否 | 配合 If-Match/ETag 做并发控制 |
| 删除资源 | DELETE | 是 | 否 | 批量删除建议用 POST 接口 |
| 探测资源支持的方法 | OPTIONS | 是 | 否 | 也是 CORS 预检的核心 |
| 链路诊断 | TRACE | 是 | 否 | 生产环境默认关闭 |
| 建立隧道 | CONNECT | 否 | 否 | 常见于代理服务 |
