K 的一隅

网络 网络基础与可靠性

HTTP 报文与状态码:服务器到底在说什么

像拆一封格式严格的商务信,读懂 HTTP 方法、请求头、响应头、状态码和响应体各自负责什么。

9 分钟阅读 更新于 2026-07-24
本文目录

一封正式商务信不会把“请报价”“附件是什么”“多久回复”全写成一团。它有称呼、主题、正文和落款。HTTP 报文也有类似的层次:起始行说明要做什么,Headers 提供上下文,Body 承载具体内容。

一封请求长什么样

http
POST /orders HTTP/1.1
Content-Type: application/json
Authorization: Bearer ...

{"productId":"p1","quantity":1}

方法表达意图:GET 通常读取,POST 通常创建或触发处理,PUT/PATCH 更新,DELETE 删除。但真正含义仍由接口契约决定,不能只凭方法名猜业务。

Headers 是上下文,不是装饰

Content-Type 说明正文如何解析,Accept 表达客户端希望收到什么,Authorization 携带身份凭证,Cache-Control 影响缓存。请求头和响应头共同构成双方对这次传输的约定。

不要把所有信息都塞进 Header。需要被服务器处理的业务内容通常放在 Body;需要出现在 URL 中、并且适合分享或缓存的筛选条件才放 Query 参数。

状态码像回信上的印章

2xx 表示请求在 HTTP 层完成,3xx 表示还要去别处或可以使用缓存,4xx 通常说明请求方有问题,5xx 表示服务器处理失败。它们是分类,不是业务说明书。

text
201 Created       创建完成
204 No Content    完成但没有响应体
304 Not Modified  使用缓存副本
400 Bad Request   请求格式或参数不合法
401 Unauthorized  缺少有效身份凭证
403 Forbidden     身份存在但无权访问
404 Not Found     目标资源不存在
429 Too Many      请求过于频繁
500 Server Error  服务器内部失败

401 和 403 的区别很有用:前者常常意味着“先证明你是谁”,后者是“知道你是谁,但不让你做这件事”。实际项目仍应以接口约定为准。

JSON 不是 HTTP 的一部分

HTTP 只负责传输,JSON、HTML、图片和流式文本都是可能的内容格式。Content-Type 是关键提示;如果服务器把 JSON 标成 text/plain,客户端仍可能拿到文本,但自动解析和安全策略会受到影响。

方法背后的安全与幂等

GET 通常被设计为读取,重复执行不应改变资源;PUT 常被设计为用一个明确地址替换资源,重复发送同样内容结果应一致;POST 往往会创建新资源或触发动作,重复发送可能产生两笔订单。HTTP 方法本身不是魔法,真正的幂等性仍要靠接口实现和业务约束保证。

这也是为什么“失败就重试”不能只写一个通用拦截器。GET 请求在网络断开后通常可以谨慎重试,支付、扣库存、发送消息则需要幂等键、请求流水号或服务端状态查询,避免客户端不知道结果时再次执行副作用。

头部决定缓存、压缩与跨域

Content-Encoding: gzipbr 表示响应体经过压缩,浏览器会自动解压;Vary: Accept-Encoding 告诉缓存不同请求头可能对应不同版本;Access-Control-Allow-Origin 则参与浏览器的跨域读取判断。它们都不在 JSON 里,却可能决定响应能否被读取、复用和正确解释。

响应头还可能表达安全策略,例如 Content-Security-PolicyStrict-Transport-SecurityX-Content-Type-Options。这些头不是“前端加一个配置就结束”,需要和资源来源、部署方式及后端返回保持一致。

状态码不是业务状态机

订单“支付中”“已发货”“已取消”不应该被粗暴映射成一堆 HTTP 状态码。HTTP 状态码回答的是这次请求在协议层发生了什么,业务状态应该在响应体或资源表示中表达。一个查询订单的请求返回 200,订单本身完全可能处于 pending

同理,接口返回 200 并不意味着前端应该展示成功提示。服务端可能在响应体里返回业务错误,或者返回了结构不完整的数据。客户端应同时检查 HTTP 层、数据结构和业务状态,而不是只写 if (response.ok)

解析失败也属于协议问题

服务器返回 200,但正文不是合法 JSON,response.json() 仍会抛异常;返回空正文却声明 JSON,也可能让解析失败。接口调试时要同时看 Status、Headers 和 Preview/Response,不能只截图状态码。

状态码之所以有价值,是因为它把一次请求拆成了协议结果和业务结果两层。协议层先回答“服务器有没有理解并处理这次请求”,业务层再回答“订单、用户或文件现在是什么状态”。如果把两层压成一个 200,客户端就必须阅读每种接口自定义的错误字段;如果把所有业务失败都映射成 500,监控和重试系统又会把正常的参数错误误判成服务器故障。

所以阅读报文时,要同时看语法、语义和后果:它怎样表达,服务器怎样解释,客户端接下来会怎么做。

一个可维护的接口通常会把这些判断写进文档、类型和监控,而不是让每个调用者凭经验猜。请求体校验、错误结构、状态码、缓存头和版本兼容共同组成接口的真实契约;少掉任何一块,系统就会在异常路径上暴露隐含假设。

报文格式的意义也在于让中间层能够工作。代理可以根据方法和缓存头决定是否复用响应,网关可以依据 Content-Length 和编码限制请求体,浏览器可以依据 Content-Type 选择解析器。应用正文只是整个协议的一部分,真正稳定的接口契约必须同时考虑起始行、头部、正文和连接行为。

请求方法还影响工具和基础设施的默认行为。GET 更容易被浏览器预取、缓存和分享,POST 通常需要读取请求体并经过更严格的日志脱敏;DELETE 可能被网关限制,PATCH 可能要求服务器支持部分更新。接口设计不是把动词换成英文,而是明确重复执行、缓存、权限和审计的语义。

响应体的结构也需要版本意识。字段新增通常比字段改名安全,枚举扩展可能让旧客户端进入未知分支,直接删除字段则可能让缓存中的旧页面崩溃。错误结构应当稳定,成功结构也要允许客户端忽略暂时不认识的字段。HTTP 只负责把版本传过去,兼容策略仍由接口双方维护。

代理和网关会改变你以为的报文。它们可能终止 TLS、追加追踪头、限制上传大小、重写 Host、缓存响应或返回自己的错误页面。看到 502,不一定是应用主动返回了 502,也可能是网关无法连接上游。排查时要确认响应来自哪一层,并用 request id 对照入口日志和应用日志。

接口契约要能被机器验证

只在文档里写“返回用户列表”还不够。客户端需要知道字段类型、是否必填、空值如何表示、分页如何计算、错误结构是什么。OpenAPI、JSON Schema 或运行时校验可以把契约变成机器可检查的东西,减少“服务端加了一个字段,前端却把它当字符串”的隐性问题。

类型声明也不等于运行时验证。TypeScript 编译后不会替你检查网络返回值,真正到达浏览器的 JSON 仍可能缺字段、类型错误或被代理替换。关键边界可以使用 schema 校验,再把未知字段和错误结构记录下来。

压缩和分块的原理

响应可以压缩,是因为正文里有重复模式;压缩算法把重复内容用更短的表示替换,浏览器依据 Content-Encoding 还原。压缩不是越高越好:小响应节省不了多少字节,复杂 JSON 的压缩 CPU 成本也可能超过网络节省。

Chunked transfer 或流式响应允许服务器在不知道最终长度时分段发送。客户端可以边收到边处理,但不能把每个 chunk 当作一个完整 JSON 对象。分块只是传输层面的切片,应用消息边界必须由正文格式定义。

版本升级要考虑旧客户端

服务端部署通常先于所有客户端更新,旧页面可能继续请求新服务器。新增可选字段通常安全,改变字段类型、删除字段和改变错误码则可能破坏旧代码。接口应该采用向后兼容的演进方式,或者通过 URL、Header 或媒体类型明确协商版本。

缓存让这个问题更复杂:旧 HTML 可能引用旧 JS,新 API 可能返回不同结构,Service Worker 还可能暂时继续提供旧资源。发布策略必须考虑资源一起更新、兼容窗口和回滚,而不是只看单次请求是否返回 200。

Headers 还存在大小和重复字段问题。多个代理追加 ForwardedX-Forwarded-For 时,应用不能简单相信第一项就是客户端地址;多个同名头的合并规则也因字段而异。信任代理链需要明确边界,否则日志审计、限流和安全判断都可能被伪造。

一个成熟接口的判断顺序

读到一个 HTTP 响应时,可以按四层判断:先确认传输是否完成,再确认状态码是否符合协议预期;然后检查 Content-Type、编码和结构能否解析;最后根据业务状态决定是展示、重试、提示用户还是进入下一步。每层都失败时,错误来源不同,修复方式也不同。

例如接口返回 200,但 Content-Type 错误,属于协议契约问题;Content-Type 正确但 JSON 字段缺失,属于数据契约问题;字段完整但订单状态为 pending,属于业务状态问题;业务成功但页面没有更新,才进入前端状态管理问题。把这些层次分开,日志和错误提示会自然清晰。

接口设计还要定义超时后的语义。客户端超时只说明它没有在期限内得到结果,不说明服务器没有执行。对于查询,可以重新发起;对于写入,要使用幂等键或查询操作状态。HTTP 报文描述了一次通信,业务协议还需要描述一次操作的最终一致性。

为什么接口要把边界写进协议

前端和后端之间没有共享调用栈,只有报文和约定。请求方法告诉对方意图,状态码告诉对方协议结果,响应头告诉对方如何解释和缓存,正文承载业务数据。任何一层含糊,都会把判断成本推给调用方,最后变成每个页面各写一套例外分支。

好的接口不是状态码越多越专业,而是让正常路径、失败路径、重试语义、缓存语义和版本变化都能被另一端稳定判断。协议越清楚,监控越容易分类,代理越容易正确工作,前端越不需要猜服务器到底想表达什么。接口越多人调用,隐含假设越贵;把判断写进协议,才不会让每个调用者重复猜测。

容易踩的坑

POST 也不是“一定不缓存”,最终仍看响应头和中间层。4xx 更不是“全是前端写错了”——权限、资源不存在、限流都可能是正常业务结果。请求头里挂上 Authorization 只解决“带上凭证”,不自动覆盖传输、存储、过期与权限校验整条链路。