HTTP 2xx状态码全解:200/201/202/204/206的正确用法与API设计实践
先说个我自己的经验过去几年里我 review 过的后端接口定义里十个有七个把成功和失败混在同一个返回结构里HTTP 状态码常年只用一个200 OK。这种做法不是不行但它把 HTTP 协议已经替你设计好的语义体系白白浪费掉了。客户端拿到 200 以后还得再解析业务 code 才知道到底成没成监控、网关、缓存层的判断逻辑全都得跟着绕。所以这篇我把 2xx 系列的每个状态码拿出来逐个拆不是背 RFC而是讲清楚它们各自想表达什么、什么时候该用、客户端收到以后应该怎么处理。这次覆盖成功类状态码的第一部分对应的就是 200、201、202、203、204、205、206 这七个207 Multi-Status 这类 WebDAV 扩展码后面单独讲。适合后端开发、客户端开发、测试和运维同学一起看尤其是那些正在设计新接口、或者打算规范现有接口的人。1. 2xx 到底在传达什么三个关键前提1.1 状态码首先是沟通契约其次才是数字HTTP 是客户端和服务器之间的对话协议状态码就是对话里最核心的应答语义。你发一个请求服务器用三位数字告诉你三件事请求有没有被正确理解、有没有被处理、处理的结果是什么。这三个问题分开看就是 1xx、2xx、3xx、4xx、5xx 这套分类的设计逻辑。2xx 落在请求已接收、已理解、已接受处理这个区间但并不保证业务层面的成功。注意这里的措辞——已接受处理和处理成功在 202 那里会体现出明显差异。平日里我们司空见惯的 200 只是 2xx 家族里最出名的一个它代表成功的方式是泛化的后面要讲的 201、204、206 各自有精确到场景的语义。理解这套语义你就能在设计接口时做出更准确的选择而不是无论什么操作都甩一个 200 出去。1.2 协议成功 ≠ 业务成功这是我特别想强调的一点。很多人调试接口时看到浏览器 Network 面板里 200 就觉得通了但 200 只能说明 HTTP 层面服务器收到了你的请求并且正常返回了不代表你提交的订单真的创建成功了、你查的数据真的存在。业务上失败但 HTTP 返回 200 的情况非常普遍尤其在一些老系统里错误详情全部塞在 JSON 的code字段里。反过来也一样业务上成功了HTTP 层也可能因为网关问题返回 5xx这种情况在异步系统里尤其常见。所以看状态码必须先建立这个认知HTTP 状态码描述的是通信结果业务状态码描述的是业务结果两者是两套坐标系可以重叠但不必然一致。后面第 5 节我会专门讲这一类200 但有问题的排查链路。1.3 为什么从 2xx 开始讲4xx/5xx 需要这里打底整个状态码体系里4xx 和 5xx 是最容易博眼球的——谁没被 404、500、502 折磨过呢。但真要区分这是客户端的错还是服务端的错前提是你得先搞清楚正常完成到底长什么样。2xx 就是那个正常完成的基准线。拿断点续传来说很多人只盯着Accept-Ranges: bytes和Content-Range看却忽略了一个关键状态码206 Partial Content。没有 206下载工具就无法区分服务器给了全部内容和服务器给了部分内容断点续传就只能靠猜。所以 2xx 不只是返回成功那几个字它是整个 HTTP 语义体系里最基础的地基。这篇把每一个 2xx 的边界划清楚后面再聊 3xx、4xx、5xx 才有对照物。2. 200 / 201 / 204最常用的三兄弟怎么用才不越界2.1 200 OK默认成功的分量200 OK是 HTTP 语义里最通用的成功响应表示请求方法已执行成功。但通用不等于敷衍它在不同方法下其实有细微差异GET返回请求资源的实际内容body 里放资源表示。HEAD和 GET 语义相同但响应不能有 bodyContent-Length头仍应给出如果 GET 时会返回的字节数。POST返回操作结果的描述或新资源的引用body 内容由接口设计决定。PUT通常返回更新后资源的表示也可以返回 200 操作描述如果不想传 body更推荐 204。DELETE成功删除后通常用 204如果服务端想告诉客户端删除操作完成了这里是结果详情也可以用 200但很多人不推荐这么做因为客户端还得处理 body。200 是一切成功的默认值但它也是最容易成为掩盖问题的挡箭牌。很多接口无论参数是否缺失、数据是否存在、服务是否报错只要请求到达了业务层就一律回应 200 业务错误码。这种设计让网关、监控、统一异常处理完全失效所有错误判断全部压到业务代码里。作为协议使用者你应该把 200 留给真正处理成功、且要返回表示的场景。2.2 201 Created创建资源时的正确姿势201 Created表示请求成功且服务器创建了一个新资源。最典型的场景是 RESTful 架构里的 POST 创建POST /api/users Content-Type: application/json { username: alice, email: aliceexample.com }服务器创建成功后返回HTTP/1.1 201 Created Location: /api/users/42 Content-Type: application/json { id: 42, username: alice, email: aliceexample.com }注意Location头它指向新建资源的具体 URI这是 201 区别于 200 的核心信号。客户端拿到 201 后如果做列表刷新或详情跳转直接从Location取值即可不用自己拼 URL。PUT 操作如果允许客户端指定资源 ID并且创建成功RFC 同样允许返回 201。实际设计 API 时建议遵循这个原则凡是这次请求在服务器上创造了新东西的都优先 201而不是笼统的 200。这能让调用方明确感知副作用也为将来的审计日志、缓存策略留出区分度。2.3 204 No Content空响应也有讲究204 No Content表示请求成功但响应没有 body 可返回。很多人写接口时删个数据、改个配置明明不需要返回内容却硬要返回一个{success: true}其实就是没用好 204。典型场景是 DELETEDELETE /api/users/42成功后返回HTTP/1.1 204 No Content Content-Length: 0204 的意义在于空 body 本身就是明确的成功信号客户端根本不需要解析内容、不需要判断 code看到状态码就可以收工。它能减少响应体积、简化客户端逻辑还能让监控系统直接通过状态码统计成功/失败。但有一个坑必须提醒204 响应里不能有 bodyContent-Length需要是 0或者干脆不返回内容。有些框架会自动给 204 增加一个空字符串 body这在某些老客户端上会触发连接复用异常的诡异 bug所以组内做接入层时最好统一规范。2.4 三兄弟对比与选型建议状态码核心语义是否带 body常见方法一句话选型建议200 OK请求成功返回结果通常带GET、POST、PUT有内容要返回时选它201 Created请求成功创建了新资源带且含 Location 头POST、PUT服务器创建出新东西时选它204 No Content请求成功但无内容返回不带DELETE、PUT、PATCH操作成功但不用回显时选它如果 DELETE 之后还想让客户端知道删了哪条、还剩多少那就返回 200 JSON如果只是想表达删掉了完事204 最干净。同理PUT 更新完不需要返回完整资源时204 比 200 更准确。这一层选择不复杂但很多项目从一开始就没约定接口文档里全写成功返回 200时间一长全乱套。3. 202/203/205/206存在感不高但关键时刻很顶用的状态码3.1 202 Accepted异步任务的入场券202 Accepted表示服务器已经收到请求但还没有处理完成最终结果可能成功也可能失败。它和 200/201 最大的区别在于202 不代表请求已经办完只代表收到并受理了。典型场景是消息队列和批处理用户提交一个大数据导出任务服务端把任务丢进队列立刻返回 202同时带上一个任务查询地址。客户端随后通过轮询或回调获取结果POST /api/exports Content-Type: application/json { start: 2024-01-01, end: 2024-12-31 }服务器返回HTTP/1.1 202 Accepted Location: /api/tasks/export-001 Retry-After: 5这里的Retry-After是给客户端的一个建议轮询间隔单位秒没有它客户端就只能自己猜。设计异步接口时我建议至少返回三样东西任务 ID、查询地址、建议轮询间隔。否则客户端要么轮询过密给服务端造成压力要么轮询过疏让用户体验延迟。还有一点容易被忽略202 不承诺最终成功任务最终可能以失败告终。因此客户端拿到 202 后不能直接更新 UI 为完成状态而是先显示已提交等后续查询接口返回终态再更新。这恰好呼应了前面说的协议成功 ≠ 业务成功——202 连协议层的完整处理完成都还没到。3.2 203 Non-Authoritative Information代理动过手脚时怎么声明203 Non-Authoritative Information是 2xx 家族里比较冷门的一个意思是响应经过中间代理修改信息来源不再是原始服务器的权威返回。翻译成大白话你请求原始站点但中间有个代理把 body 或 header 改了代理就用 203 告诉你这内容不是源站原样给的是我处理过的。典型场景是透明压缩代理、内容转码网关、在 HTML 里注入脚本或广告的中间层。现实里很少有人见到 203原因是大多数代理贪图省事直接把自己的响应伪装成 200 返回这会让客户端误以为收到的就是源站的原始内容。从规范角度看代理修改响应内容属于合法但应声明的行为203 就是那个声明机制。虽然日常开发中用得少但在做网关、SDK 接入时如果发现上游响应被中间层改过就应该了解这个状态码的存在至少别人抛出 203 时你不会觉得这是什么鬼。3.3 205 Reset Content表单时代的遗产205 Reset Content和 204 很像也是成功且无 body但它额外要求客户端重置文档视图。放在 HTML 表单时代就是用户提交完表单后浏览器要自动清空表单字段让页面回到初始状态。早期的信息交互系统里表单提交大多走页面刷新205 就是那个清空重来的信号。现在的前后端分离架构里表单提交普遍走 Ajax/SPA重置表单状态是前端自己管理的事205 几乎没有存在感。不过我确实在处理老系统对接时看到过——某些嵌入式管理后台仍会用 205 告诉 Web 页面提交成功请重置表单。如果你也遇到这种情况把它当成 204 的变体处理即可唯一多出来的动作是重置页面输入区域不要留在用户面前继续显示旧数据。3.4 206 Partial Content断点续传与流媒体播放的地基206 Partial Content是 2xx 里最能干的一个它表示服务器只返回了资源的一部分通常配合Range请求头使用。它解决的是一类很实际的问题文件下载一半断了、视频从中间开始播、日志文件只想取尾部 100 行——这些场景如果服务器每次都返回完整内容带宽和体验都扛不住。一次典型的范围请求交互长这样客户端先发一个 HEAD 或 GET 请求服务器在响应里声明自己支持范围请求HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 10000然后客户端断点续传时带上 RangeGET /bigfile.zip Range: bytes5000-服务器返回HTTP/1.1 206 Partial Content Content-Range: bytes 5000-9999/10000 Content-Length: 5000关键点在于Content-Range头它必须包含当前返回的字节区间和总大小格式是bytes 起始-结束/总长度。没有它客户端就不知道这一段在整个文件里的位置无法拼接数据。206 的进阶形态叫多段范围响应。客户端可以一次请求多个不连续的片段Range: bytes0-100, 500-600服务器需要返回Content-Type: multipart/byterangesbody 里按 multipart 格式分段携带每一块数据。Nginx、CDN、主流下载工具对单段 Range 的支持都很成熟但多段 Range 的兼容性参差不齐自己实现下载工具时最好先做兼容测试。这里有一个常见的坑服务器收到 Range 请求但选择忽略直接返回 200 完整内容这是合法的。所以下载工具不能假定发了 Range 就一定拿到 206必须同时处理 200 和 206 两个分支。判断标准就是状态码本身——拿到 206 才按分片逻辑拼接拿到 200 就整体覆盖。4. 2xx 响应里的头、体和缓存控制规则4.1 body 带不带状态码说了算很多接口设计者习惯有返回就一定有 body导致不该有 body 的 204/205 也被塞了东西或者该有 body 的 200 却空手而归。判断标准其实就一条看状态码的语义。200、201、203、206通常带 body分别承载资源表示、新资源表示、修改过的资源表示、分片内容。202可以带一个任务描述体也可以只返回 Location 头。204、205必须无 body这是协议层面的硬性要求。body 的格式由Content-Type决定。现在大多数 JSON API 会用application/json但规范推荐的错误表示格式是application/problemjsonRFC 7807它定义了type、title、status、detail、instance这些字段用来更清楚地描述错误。虽然现在主流团队还没完全普及但如果你在设计公共 API我很建议了解一下它远比各家自造的{code, message, data}更有通用性。4.2 Content-Type、ETag、Cache-Control 和 2xx 的配合2xx 不只是告诉客户端成功了它还承载着缓存协商、多版本控制等能力。这里重点说三个头ETag给资源一个版本标识。客户端带着If-None-Match: abc123再来访问时如果版本没变服务器返回304 Not Modified如果变了返回 200 新资源 新 ETag。这套机制在 2xx 响应里扮演的是资源指纹角色。Last-Modified/If-Modified-Since用时间戳做版本判断精确到秒是 ETag 的廉价替代方案。精度要求高的场景还得靠 ETag。Cache-Control告诉客户端和缓存代理这个 2xx 响应能不能缓存、能存多久。no-cache并不是不允许缓存而是用之前必须回源验证max-age3600表示 1 小时内直接使用本地缓存。有一个实操细节值得单独说对 200 和 206 的缓存策略要区别对待。206 响应通常缓存的是分片数据HTTP 规范要求缓存系统能把多个分片拼成完整资源但很多自建的缓存层并不支持导致文件下载完毕后缓存里还是碎片。如果你自己写缓存中间件要么默认不对 206 做持久化缓存要么实现完整的片段合并逻辑。4.3 一个模板符合规范的 2xx 响应长什么样以用户更新个人资料为例展示不同情况分别该返回什么场景一更新成功且返回更新后的完整用户信息。PUT /api/users/42 Content-Type: application/json { nickname: new_name }HTTP/1.1 200 OK Content-Type: application/json ETag: u42-rev3 Cache-Control: max-age60 { id: 42, nickname: new_name, updated_at: 2025-03-10T10:00:00Z }场景二更新成功但客户端不需要看到更新后的内容比如只是切换一下开关。HTTP/1.1 204 No Content ETag: u42-rev4场景三更新请求已受理但需要异步处理比如头像要转码压缩。HTTP/1.1 202 Accepted Location: /api/tasks/avatar-42 Retry-After: 3小技巧204 响应也可以带 ETag而且客户端可以把新版本号存下来下次配合If-None-Match使用。很多团队忽略了这一点以为 204 就是光秃秃一个状态码其实头字段仍然有发挥空间。5. 抓包看到 200 却报业务错误这类问题的排查链路5.1 先分清协议成不成功和业务成不成功这是排查所有 HTTP 问题时的第一道分岔路。你在浏览器 Network 面板看到HTTP/1.1 200 OK Content-Type: application/json {code: 50001, message: 订单创建失败}这个场景下协议层完全成功业务层明确失败。问题大概率出在业务逻辑、参数校验、依赖服务超时等业务代码里。排查方向应该转向应用日志、上游依赖、数据库状态而不是在 HTTP 语法、Header、网关配置上浪费时间。反过来如果你看到的是HTTP/1.1 502 Bad Gateway那就说明问题出在通信链路网关连不上上游、上游响应超时、上游崩溃。这时候再去看业务代码是空转的正确姿势是检查服务健康状态、负载均衡配置、容器存活情况。这个区分听起来很简单但实际运维时我发现很多人会混着查200 的报错去查网关配置502 的报错去翻数据库日志方向错了后面全是无效劳动。5.2 核查服务端是否滥用 200 吞掉错误语义排查询不到结果时下一步要做的就是拷问服务端的状态码设计。我见过太多内部系统整个接口全是 200只有一种例外——请求没走到业务层被网关直接拦下的才算 400/500。这种做法导致的后果就是状态码信息熵为 0所有问题都靠解析 body 里的 code 字段。如果你在自己项目里排查遇到所有响应都是 200可以先看看服务端是否统一做了try { 业务 } catch { 返回 200 错误码 }这层包装。如果是那你缺的不是排错手段是接口语义规范。正确的做法是参数校验失败 →400 Bad Request未登录/无权限 →401 Unauthorized/403 Forbidden数据不存在 →404 Not Found并发冲突 →409 Conflict服务端抛异常 →500 Internal Server Error这样客户端和网关才有机会利用 HTTP 本身的语义做出判断出错时也能第一时间分清责任方。5.3 把状态码和代理层、浏览器调试串起来看一个请求从浏览器到目标服务器中间可能经过自己的接入层、网关、CDN、负载均衡。每一层都可能改写状态码这是排查时必须考虑的。举例源站正常返回404 Not Found但 CDN 缓存了 404 并设置了很长的缓存时间后续所有同类请求都会直接从 CDN 返回 404连源站日志都看不到。另一个例子Nginx 做proxy_intercept_errors on时会把上游返回的 404/500 统一替换成自定义错误页状态码可能会变成 200。如果客户端收到 200 但页面内容却是错误提示就要怀疑中间层做了错误页拦截。排查这类问题最有效的方法是分跳观察curl -I https://example.com/api/users/999逐层对比响应码分别查看 DNS 解析、CDN 节点、接入层、源站的实际返回值。看到哪一层状态码变了问题就定位在哪一层。这也是为什么字段X-Cache、Via、X-Request-Id这类链路追踪 Header 很重要——没有它们你连是谁改的状态码都查不出来。6. 工程实践设计 API 时如何正确地给出 2xx6.1 别做一律 200派语义正确带来的红利业界确实存在一律 200派和语义分明派。主张一律 200 的理由通常是客户端处理逻辑简单、网络层错误和业务错误统一走一套 JSON 结构、某些老旧 HTTP 客户端对非 200 状态码处理不友好。这些理由有一定历史背景但在现代体系里弊大于利。语义正确带来的红利是实实在在的监控告警可以直接按状态码区间统计错误率不用解析业务日志。重试策略可以区分对待5xx 大概率是临时问题可以重试400 是客户端参数问题重试没意义。API 网关可以做统一鉴权、限流、熔断错误发生时能定位到具体环节。新接入的客户端开发者可以从状态码一眼看出问题归属不需要逐行读业务码。我的建议是HTTP 状态码表达协议结果body 里的 code 表达业务细节两者各司其职。用 200 承担所有结果、只靠 code 区分等于把一层本来免费可用的语义信息给扔掉了。6.2 真实的抉择场景数据不存在到底返 200 还是 404这是接口设计里争论最多的问题之一。我自己的判断标准很简单这个接口的语义是读取一个具体资源还是执行一个操作、资源不存在只是业务分支。如果是 GET /api/users/42资源不存在返回 404 是符合语法语义的做法。客户端只要看到 404 就明确没有这个资源不需要再解析业务码。可问题是很多业务场景里用户不存在不是异常情况而是正常业务分支——比如登录时用户不存在下一步应该走注册流程把 404 抛给客户端客户端就要多做一层判断。这种情况下业界有一种折中做法业务层仍然返回 200 业务码如code1001表示用户不存在HTTP 层保持 200。理由是请求本身是成功的服务器成功处理了验证逻辑只是验证结果是否定。另一种做法是坚持 404客户端把 404 当成资源不存在的一种业务信号。这两种方案没有绝对的对错但团队内部必须达成一致并写进接口规范。如果文档里从来说不清楚新同事入职后做的第一件事就是踩这个坑。6.3 2xx 之外还要注意的几个雷区设计 2xx 响应时有几个雷区值得单独提醒雷区一POST 创建成功却返回 200。如果客户端需要知道新资源 ID200 JSON 也能做到但状态码无法告诉客户端这个请求创建了资源。做了缓存或幂等设计时201 和 200 的可观测性差异会很明显。雷区二PUT 更新成功返回 200 空 body。要么返回更新后的完整资源要么直接 204夹在中间会让客户端困惑body 解析失败算不算操作失败统一规范后客户端判断逻辑会简单很多。雷区三DELETE 返回 200 JSON 但 JSON 里永远只有{success: true}。完全可以用 204 替代缩短响应体。雷区四异步任务一律 200。一旦进入任务已提交实际结果在后面的模式务必用 202。否则调用方会以为请求已经完成后续查询逻辑根本不会触发。还有一点关于幂等性2xx 和幂等设计没有直接绑定关系但如果你用 POST 创建资源并返回 201却发现由于网络重试产生了多条重复记录那不是状态码的错是接口没有做幂等键处理。可以要求客户端在请求头里带Idempotency-Key服务端针对该 key 去重。这个从工程上比纠结200 还是 201更重要但两者配合起来API 才是完整的。6.4 状态码与业务码的共存模板如果要在文章里给一个可直接抄的模板我建议用字段status表示 HTTP 状态码用code表示业务码。举个例子{ status: 409, code: USER_EMAIL_DUPLICATED, message: 该邮箱已被注册, trace_id: f47ac10b-58cc-4372-a567-0e02b2c3d479 }HTTP 层返回 409 而不是 200是因为邮箱重复不是服务器故障也不是语法错误而是请求与当前资源状态冲突。RFC 9110 里明确有409 Conflict这种语义用它比 200 错误码更精确。同时把 trace_id 带上排障时能把客户端看到的错误和服务器日志串到同一条链路上省去无数沟通成本。这里的核心思路是让状态码承担粗粒度的分类让业务码承担细粒度的定位让 trace_id 承担链路追踪。三者结合既照顾了 HTTP 生态的通用性也照顾了业务系统的特异性。批量导入接口的取舍也值得一提如果一批 1000 条数据里有几条校验失败整批返回 400 会让已成功的也回滚全部返回 200 又掩盖了部分失败。我见过比较合理的方案是返回 200 汇总结果 JSON成功 N 条、失败 M 条、每条失败的原因但前提是这批任务的接受语义确实成功处理细节作为业务信息返回。如果导入后还要跑异步转换流程那就该 202 打头结果通过查询接口暴露。判断标准始终是请求本身完成了没有。结尾一点个人习惯做了这么多年接口设计我 review 别人的代码时第一件事就是看每个接口的状态码覆盖面成功是哪几个码、失败是哪几个码、冲突用哪个码、异步用哪个码。如果这些定义不清晰不管代码写得多少漂亮我都觉得这个接口还没想明白。最后分享一个实用小技巧调试 2xx 语义时可以在命令行用 curl 快速验证响应头和行为是否符合预期。比如检查 206 是否正确可以这么测curl -I -H Range: bytes0-99 https://example.com/largefile.bin curl -H Range: bytes0-99 -D - -o /dev/null https://example.com/largefile.bin第一条看服务器是否声明Accept-Ranges第二条看最终返回的是206 Partial Content还是200 OK配合Content-Range确认分片区间。这套检查做熟了你对 2xx 的理解就会从背定义变成看行为遇到接口异常时定位速度会快一个量级。2xx 是把成功说清楚的一组语言这一篇把最常用的七种基本讲透了后续聊 3xx 重定向和 4xx/5xx 的时候我们再拿这套成功基线去对照各自的反面。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →