刚过去的一周里,我在微信群里看一位做网关的同学截图求助:unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:15721/v1/responses。截图下面跟着一堆人的猜测——有人说是后端进程挂了,有人说是端口不通,还有人直接甩给他一个"重启试试"。但真正的问题在报文里写得明明白白:502意味着网关拿到了上游服务器的非法响应,不是连不上,而是连上之后得到的回复不符合HTTP协议规范。这种场景我见过太多次了。HTTP、HTTPS、请求头、响应头、状态码、数据包结构,这些基础概念看起来每个开发者都"知道",但真正遇到问题能快速定位的人并不算多。这篇不是教科书式的名词解释,而是想带着你从协议的角度把HTTP/HTTPS重新拆一遍,顺便聊聊我这些年用这套知识解决过的实际问题。适合刚入门想系统梳理的后端/测试同学,也适合被各种灵异网络故障折磨了一阵子的老开发。
1. 从一次502故障说起:协议理解是排查问题的底层能力
1.1 故障现场的报文长什么样
先把这个502事故拆开。unexpected status 502 bad gateway: unknown error这行报错里,有几个关键信息:
502是HTTP响应状态码,全称 Bad Gateway,意思是"作为网关或代理角色的服务器,从上游服务器收到了无效响应"。url: http://127.0.0.1:15721/v1/responses说明网关把请求转发到了本机的15721端口。unknown error是上层应用对上游异常的一种笼统表述,它掩盖了真正的原因——上游到底返回了什么、连接是否被重置、响应是否超时。
如果你的知识只停留在"502是服务器错误",那就只能去重启进程碰运气。但如果你能理解HTTP的请求-响应模型、状态码语义、以及网关与上游之间的交互方式,你的排查路径会立刻清晰起来:先确认15721端口是否有进程监听,再手动用curl不带任何额外头去请求上游,观察它返回的状态行和响应头,判断问题是出在连接阶段还是协议解析阶段。
1.2 想看懂这些,需要哪几块知识
从这次排查就可以看出,HTTP/HTTPS协议知识其实是一套可以用在排障现场的技能组合,它大致包含四块:
- 请求行的结构与请求头语义——你发给服务器的每一行,服务器如何理解。
- 状态行与响应头语义——服务器的回复该怎么读懂。
- 数据包组织方式——HTTP报文在TCP流里怎么切分、怎么拼装。
- HTTPS的TLS加密层——为什么抓包看到的不再是明文,以及如何判断加密握手的成败。
这四块知识不是孤立的。请求头里的Host、Connection、Content-Length直接决定了服务器怎么解析你的请求;状态码与响应头里的Content-Type、Transfer-Encoding决定了客户端怎么解析响应;而数据包结构决定了你在Wireshark里看到的到底是完整报文还是半截数据流。下面按这条脉络逐层拆开。
2. 请求头拆解:每一行都在告诉服务器"我是谁"
2.1 先看一段真实请求的原始报文
我随便抓一段用curl访问网站时发出的HTTP/1.1请求,去掉无关部分后大致是:
GET /api/user/info HTTP/1.1 Host: www.example.com User-Agent: curl/8.0.1 Accept: */* Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Connection: keep-alive Cookie: sessionid=abc123; theme=dark这段报文的第一行是请求行,由三部分组成:请求方法(GET)、请求目标(/api/user/info)、协议版本(HTTP/1.1)。请求行之后是若干请求头字段,每个字段都是字段名: 字段值的键值对形式,最后跟一个空行,空行表示请求头结束。
很多人写代码从来不看原始报文,因为工具(浏览器、Postman、curl)把这些都包装好了。但遇到问题时,你必须在脑子里能"看见"这坨文本。
2.2 必须存在的Host头与虚拟主机机制
HTTP/1.1协议规定,请求必须携带Host头。它的用途是告诉服务器:你访问的是哪个域名。为什么需要它?因为一台服务器上常常同时部署多个网站(虚拟主机),它们共享同一个IP和端口。如果没有Host头,服务器无法区分用户要访问的是哪个站点。
举一个线上案例:某次排查发现,用公网IP直接访问后端服务一切正常,但用域名访问就返回403。原因就是Nginx配置了多个server_block,每个都绑定了不同的server_name,缺少Host头或Host头不匹配时,Nginx会落入默认server块,而默认server块拒绝了所有请求。排查手段很简单:curl -v http://1.2.3.4/ -H "Host: www.example.com",手工指定Host头来验证。
从上手经验来看,初学者最容易忽略的一个点就是:改Hosts文件做本地联调时,如果目标服务器上配了严格的域名校验,你必须在请求头里带上正确的Host,否则会看到各种莫名其妙的403/404。
2.3 内容协商头:Accept系列决定服务器返回什么
Accept、Accept-Encoding、Accept-Language这三个头经常被忽略,但它们在真实业务里表现得非常活跃。
Accept: application/json告诉服务器,客户端能接受application/json类型的响应。如果服务器只能返回text/html,它可能返回406 Not Acceptable,或者忽略你的期望直接返回HTML——这取决于服务端实现。Accept-Encoding: gzip, deflate, br声明客户端支持的压缩算法。线上环境里,绝大多数响应经过Nginx后都会被gzip压缩,体积能减少60%以上。这个头缺失时,服务器可能返回未压缩内容,流量翻倍;反过来,如果客户端解析压缩内容有bug,也会表现为乱码。Accept-Language影响多语言站点返回哪种语言的页面。
我遇到过最典型的坑是:用Python requests库请求某个接口,默认带上了Accept-Encoding: gzip, deflate,但忘记解压响应体,导致打印出来一堆乱码字节。这不是协议问题,而是对内容协商机制理解不完整。
2.4 Authorization与Cookie:两种完全不同的身份机制
Authorization头用于携带凭证,常见形式是Authorization: Bearer <token>或Authorization: Basic base64(user:password)。它的特点是每次请求都要主动带上,服务器不"记住"你,纯粹靠这个头来识别身份。
Cookie头则不同。它的值来自服务器之前通过Set-Cookie响应头下发的身份标识,浏览器会自动帮你保存并在后续请求中自动携带。所以在抓包时你会发现,带Cookie的请求不需要在代码里手动拼Cookie头,浏览器全包了。
接口联调时,有个高频问题与这两套机制有关——文件下载。热搜词里有一条"a标签下载视频请求头怎么带token",恰恰是Authorization和Cookie都覆盖不了的场景。<a>标签发起的下载请求无法自定义请求头,如果下载接口依赖Authorization头鉴权,a标签下载必然401。业界常见的绕法有两种:
- 用
fetch或XMLHttpRequest请求接口,带上Authorization头,拿到Blob后用URL.createObjectURL生成临时链接,再触发下载。 - 把token拼到URL查询参数里(安全性较差,token会进访问日志,不推荐但常见)。
实操建议:下载接口尽量同时兼容Cookie会话和短时效签名参数,这样既能避免token入日志,又能兼容a标签直链下载。
2.5 调试请求头时的三个实用动作
- 用
curl -v或curl -i查看完整请求/响应头。-v会输出握手、请求行、响应行全过程,是最常用的排障利器。 - 修改或伪造请求头,用
curl -H "X-Custom-Header: value"。测试签名、风控、限流逻辑时经常要用。 - 抓包工具选型:轻量场景用浏览器DevTools的Network面板,需要看TLS握手或修改请求再转发时,再用Charles、BurpSuite这类工具。JMeter录制HTTPS脚本时,也是靠JMeter里的HTTP(S) Test Script Recorder起一个本地代理,浏览器把流量交给代理,JMeter再自动生成请求。这背后的原理,就是代理把浏览器发来的HTTP请求解析后转成JMeter采样器,整个过程实际上就是在处理请求行和请求头。
3. 响应头与状态码:读懂服务器回话的语法
3.1 状态行的结构与状态码整体分布
服务器返回的报文,第一行是状态行,格式为HTTP版本 状态码 原因短语。比如HTTP/1.1 200 OK、HTTP/1.1 502 Bad Gateway。状态码是机器可读的三位数字,原因短语是人类可读的描述,但客户端程序应该只信任数字,不要解析原因短语做逻辑判断——不同服务器实现的原因短语可能不完全一样。
状态码按首位数字分为五类,这是理解HTTP错误的基础框架:
| 分类 | 范围 | 一句话语义 | 典型代表 |
|---|---|---|---|
| 1xx | 100-199 | 信息性响应,请求还在处理中 | 100 Continue, 101 Switching Protocols |
| 2xx | 200-299 | 请求成功 | 200 OK, 201 Created, 204 No Content |
| 3xx | 300-399 | 重定向,需要进一步操作 | 301, 302, 304 Not Modified |
| 4xx | 400-499 | 客户端错误,问题出在请求本身 | 400, 401, 403, 404, 429 |
| 5xx | 500-599 | 服务端错误,服务器处理请求时出问题 | 500, 502, 503, 504 |
3.2 高频状态码的现场处置经验
200 OK不代表业务成功。这一点特别重要,尤其在联调阶段。HTTP层的200只表示"服务器正常处理了HTTP请求",但响应体里的{"code": 500, "msg": "服务异常"}是业务层的失败。我看到太多人把HTTP状态码和业务状态码混为一谈,接到400就怀疑网关拦截,接到200就以为接口成功——这两种判断都可能是错的。
301和302是重定向。301是永久重定向,浏览器会缓存这个跳转;302是临时重定向。平时接触最多的场景是HTTP跳HTTPS、域名迁移、登录后跳回原页面。排查时用curl -I看响应头里的Location字段,就能看到跳转目标。
304 Not Modified用于协商缓存。请求头里的If-None-Match(配合响应头ETag)或If-Modified-Since(配合响应头Last-Modified)命中时,服务器返回304,表示"你可以继续用本地缓存"。它不含响应体,如果你发现某些静态资源请求总是走304,说明缓存策略生效,这是正常的。
400 Bad Request是服务器无法理解请求。常见成因包括:请求头字段值格式非法、JSON体解析失败、Content-Length与实际body长度不匹配。有些服务端框架对请求头大小有限制,默认8KB-16KB,超了也会返回400。此时要去查你是不是往请求头里塞了太多自定义字段。
401与403的区别,是老生常谈但也总有人搞混。401是"未认证",意思是服务器不知道你是谁;403是"已认证但没权限",意思是服务器知道你是谁,但你不允许访问这个资源。实际排查时,401优先检查token是否缺失/过期,403优先检查权限配置、IP白名单、Referer校验、UA校验。
404不只是"路径不存在"。Nginx层的404往往意味着没有匹配到location规则;后端应用层的404可能是路由没注册;有些安全策略也会故意把存在的接口返回404来隐藏资产。用curl -v看一下响应头里有没有Server: nginx,再对比响应体是JSON还是HTML,基本能判断是哪一层拦截。
429 Too Many Requests是限流触发。响应头里一般带Retry-After,告诉客户端多少秒后重试。联调时如果接口突然大规模429,除了检查QPS是否超标,还要看是否多个请求共享了同一个IP出口(比如公司出口NAT),导致限流误伤。
3.3 5xx服务端错误:502和504需要分开对待
500是所有服务端错误的"兜底",任何未捕获异常都可能映射成500。但作为排查者,你要警惕一个现象:有些网关会把上游非HTTP响应也统一包装成500或502返回给客户端,这时候真正的原因被吞掉了。所以排障时不能只盯网关返回,还要直接打上游看日志。
502 Bad Gateway的准确定义:网关从上游收到了无效响应。无效包括:上游主动断开了连接、上游返回的HTTP报文格式错误、上游在响应还没结束时连接被重置。Nginx场景下最常见的三类原因:
- 上游进程崩溃或超时退出,比如PHP-FPM的
max_children耗尽、Java进程OOM。 - 上游响应头不规范,比如返回了非法的chunked编码、响应头里有非法字符,Nginx认为这不是合法HTTP响应。
- 上游地址配置错误,比如Nginx把请求转发到一个没有监听的端口,这时Nginx返回的是502而不是504。
504 Gateway Timeout的含义不同:网关已经连上了上游,但上游在规定时间内没有返回响应。排查方向是上游处理耗时、网关的proxy_read_timeout是否太短、以及是否出现了慢SQL拖垮了上游。
另外,热词里出现的http 524主要来自Cloudflare这类CDN,含义是"源站在规定时间内没有返回完整响应",本质也是超时类问题,只是CDN厂商用非标准状态码细化区分了场景。
3.4 响应头里决定客户端行为的关键字段
Content-Type:告诉客户端响应体是什么类型,application/json; charset=utf-8、text/html; charset=utf-8。charset缺失时中文容易乱码。Content-Length:响应体的字节长度。客户端依靠它判断响应是否接收完整。如果长度不对,浏览器会报ERR_CONTENT_LENGTH_MISMATCH。Transfer-Encoding: chunked:当服务器不知道响应体总长度时(比如流式输出),用分块传输而不是Content-Length。块的大小用16进制写在每块前面,最后以"0\r\n\r\n"结束。这也解释了为什么部分场景下抓包看到的响应头里没有Content-Length。Set-Cookie:服务器通过它下发Cookie,浏览器保存并在后续请求自动携带。一个响应里可以有多个Set-Cookie。Location:配合3xx状态码使用,告诉客户端新的资源地址。Cache-Control: no-cache / max-age=...:控制浏览器缓存行为。调试接口时如果发现改了代码但浏览器还在用旧值,先看响应头里的Cache-Control。Access-Control-Allow-Origin:CORS跨域相关的核心响应头。前端联调报跨域时,先确认这个字段是否包含当前来源域名,以及请求是不是预检(OPTIONS)请求。
4. 数据包结构:从TCP流到HTTP报文的现场还原
4.1 HTTP报文的基本骨架
HTTP协议是一套基于文本的请求-响应协议(HTTP/2之后改为二进制帧,这里以最常见的HTTP/1.1为例)。它的报文结构可以用四段来概括:
起始行(请求行或状态行) 请求头/响应头(多个字段,每个字段一行) 空行(CRLF) 消息主体(可选)这个结构极其简洁,但细节藏在容易忽略的地方:
- 起始行和每个头字段之间都用
\r\n(回车换行)分隔,不是单独用\n。 - 头字段与消息主体之间必须有一个空的
\r\n\r\n。 - 头字段的名称不区分大小写,但值区分大小写。
理解这个结构的一个直接用途是:任何解析HTTP报文的代码,都要先找到\r\n\r\n的位置,把头部和主体分开。索引请求头里出现的"头注入"攻击,危险的根源也正是这个\r\n\r\n——如果业务层把用户输入直接拼进响应头,攻击者注入一个\r\n就能伪造额外的响应头,注入\r\n\r\n甚至能伪造整个响应体。
4.2 TCP抓包视角下的HTTP报文
用Wireshark或tcpdump抓包时,你会发现HTTP报文不是"一个包一个HTTP请求"的干净对应关系。一个HTTP请求可能占多个TCP包,多个HTTP请求也可能合并在一个TCP包里。这是因为HTTP跑在TCP之上,TCP是面向字节流的协议,不关心上层报文边界。
我常用的抓包命令是:
tcpdump -i eth0 -s 0 -w http.pcap 'tcp port 80'然后把http.pcap拖进Wireshark,在过滤栏输入http,就能看到Wireshark帮我们解析出的HTTP请求和响应。但如果过滤栏输入http && http.request却发现有些HTTP请求没被识别,常见原因有两个:一是请求被TLS加密了(端口443,协议显示为TLS),二是TCP分段导致Wireshark的HTTP解析器没能重组完整报文——此时可以尝试tcp.stream eq 0看完整流。
顺着TCP流你会看到非常直观的现象:SYN、SYN-ACK、ACK的三次握手之后,才出现GET / HTTP/1.1这样的明文HTTP请求。URL的域名部分在DNS查询中单独出现,HTTP报文里的Host头又重复了一遍域名。
4.3 粘包、拆包与连接复用:为什么"一个连接多个请求"是常态
网络编程新手用Socket收发HTTP数据时,最容易遇到的问题就是"粘包"与"拆包"。原因还是那个:TCP是字节流,没有消息边界。发送方调用两次send分别发了"A"和"B",接收方可能一次性收到"AB",也可能只先收到"A"的一部分"哈"。解决思路不是调整TCP(TCP本来就保证顺序和可靠交付,不保证消息边界),而是在应用层自己界定边界。
HTTP/1.1里界定边界的方式主要有两种:
- 提前声明
Content-Length,接收方从请求头解析出长度N,然后从TCP流里读够N个字节,才算拿到一个完整报文。 - 使用
Transfer-Encoding: chunked,每块前面有长度前缀,读到最后一块长度为0就结束。
边界界定清楚之后,连接复用就容易理解了。HTTP/1.1默认是Connection: keep-alive,也就是同一个TCP连接上可以连续发送多个HTTP请求,避免每次请求都重新握手。这给性能带来巨大提升,但也制造了一个新问题:队头阻塞——前一个请求的响应没返回完,后面的请求即使已经发到服务器,响应也必须在队列里等。
HTTP/2引入了多路复用(Multiplexing)来解决队头阻塞:一个TCP连接上可以同时跑多个流,每个流承载一个请求-响应交换,帧之间交错传输。这也是为什么HTTP/2抓包后,Wireshark里的视图和HTTP/1.1完全不同,它不再显示明文请求行,而是一个个HEADERS、DATA帧。热词里的"http连接复用",基本指的就是这个脉络:从HTTP/1.1的keep-alive,到HTTP/2的多路复用。
我在嵌入式设备上写过HTTP图像传输(ESP01S这类低资源设备),当时用的是最简单的方式:TCP建立连接后发一个POST /upload HTTP/1.1,带上Content-Type: image/jpeg和Content-Length: <图片字节数>,body直接放图片数据。那一端只要严格按照Content-Length读完指定字节数,就能拿到完整的图片。这就是报文边界在实际设备中的经典应用。
5. HTTPS的加密层:TLS握手与证书验证的真相
5.1 为什么HTTPS需要对称加密+非对称加密搭配
HTTP是明文协议,HTTPS的本质就是"HTTP over TLS"。TLS在最开始要解决一个矛盾:
- 对称加密(如AES)加解密快,但需要一个共享密钥,而双方一开始并没有安全通道来传递这个密钥。
- 非对称加密(如RSA、ECDHE)可以公开公钥,但加解密慢,不适合加密大量数据。
TLS的方案是"混合加密":用非对称加密或密钥交换算法在握手阶段协商出一个会话密钥,随后所有业务数据都用对称加密来保护。RSA密钥交换的流程是客户端用服务器公钥加密一个随机数(pre-master secret)发给服务器,双方各自算出相同的会话密钥;而TLS 1.3更常用的ECDHE则是在握手的同时完成"前向保密"——即使服务器私钥泄露,历史上抓到的流量也无法被回溯解密。
5.2 TLS握手(1.3版本)的大致过程
我用Wireshark抓HTTPS请求时,在TLS协议层看到的主要是下面几个报文:
ClientHello:客户端发给服务器,包含支持的TLS版本、加密套件列表、随机数,以及SNI扩展(Server Name Indication,把要访问的域名以明文形式放在这个字段里)。ServerHello:服务器选一套双方都支持的加密套件,返回随机数。- 服务器发送
EncryptedExtensions、Certificate、CertificateVerify、Finished——证书链在这里下发,客户端验证证书。 - 客户端发送
Finished,后续所有应用数据都是加密的。
这个过程中最容易被忽略的是证书验证。客户端收到Certificate后要做三件事:校验证书链是否由受信任的根CA签发(本地信任库里有对应根证书);校验域名是否与证书的subjectAltName匹配;校验证书是否在有效期内。任何一环失败,浏览器都会拦截并给出安全警告。
5.3 抓HTTPS包时,你看到的"密文"是什么
很多同学首次抓HTTPS包时大跌眼镜:在Wireshark里明明看到完整的TCP连接,但应用层数据全是Application Data,看不到HTTP明文。这跟"HTTPS明文捕获"这个热词挂上了钩——其实这本身就是正常现象。TLS加密后,HTTP报文变成了TLS记录层的载荷,Wireshark不借助密钥无法解码。
不过有两个点值得注意,它们常被误解为"HTTPS泄露隐私":
- SNI是明文的。也就是说,抓包者虽然看不到你访问的URL和请求头,但能看到你连接的目标域名。
- 证书是明文的。握手阶段服务器会明文下发证书,抓包者能看到证书里的域名信息。
所以HTTPS保证的是"通信内容机密性、完整性、身份可信",并不保证"访问了哪个域名"这个元数据完全不可见。
5.4 用SSLKEYLOGFILE解密自己的HTTPS抓包
调试HTTPS接口时,如果实在需要看到报文明文,有两个思路:
一是用中间人代理(Charles、BurpSuite、JMeter录制HTTPS脚本都属此类):在客户端信任代理的CA根证书,代理解密TLS后再转发给服务器。它适用于浏览器/App调试,但需要注意,安装了不可信根证书本身有安全风险,调试完要移除。
二是利用浏览器或程序导出的会话密钥:设置环境变量
export SSLKEYLOGFILE=/tmp/tls.keys然后启动Chrome/Firefox或curl,再在Wireshark的Preferences -> Protocol -> TLS里配置(Pre)-Master-Secret log filename指向这个文件。重新抓包后,Wireshark就能解出TLS载荷里的HTTP明文。这个方法对本地调试极其好用,不用装任何代理。
6. 实战复盘:502故障与请求头注入的排查链路
6.1 502 Bad Gateway的完整排查链路
回到开头那次故障,我把完整的排查链路整理一遍,这也是我处理同类问题时的标准动作。
第一步,确认拓扑。先搞清楚客户端到目标之间到底有几层:浏览器/客户端 -> Nginx -> 上游应用?还是API网关 -> 微服务?热词里的报错来自一个特定网关程序,它把请求转发到127.0.0.1:15721,所以拓扑是"网关 -> 本机15721端口的服务"。
第二步,确认上游是否存活。执行:
ss -lntp | grep 15721 curl -v http://127.0.0.1:15721/v1/responsesss看端口是否在监听,curl直接打一次上游,观察状态行和响应头。这一步能区分"上游根本没起来"和"上游起了但响应异常"。
第三步,观察上游返回的具体内容。如果curl直接失败,看是连接被拒(Connection refused)、超时(timeout)、还是连接被重置。如果上游返回了200但网关仍然报502,那问题大概率出在网关对上游响应的二次解析上——比如上游返回的响应头里有非法字符、Content-Length与body不一致,或网关规定上游必须返回application/json而上游返回了纯文本。
第四步,翻上游日志。这里给一个实操提醒:很多502的真实原因是上游进程在处理请求时崩溃了,进程没了,端口自然被释放,Nginx在转发瞬间拿到的是Connection refused。所以日志要看两个时间点:崩溃前有没有OOM、有没有未捕获异常;崩溃后进程是否被守护进程重新拉起。
第五步,验证修复。改完配置或重启服务后,不要只测一次就完事,建议用循环脚本压几轮:
for i in $(seq 1 100); do curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:15721/v1/responses done | sort | uniq -c这样能看到是否有间歇性失败。
6.2 请求头注入的原理、危害与防御
请求头注入是热搜词里"http头注入"的正式说法,也是CTF和Web安全测试里常出现的考点,它的本质是CRLF注入。
HTTP的字段分隔符是\r\n。如果一段代码把用户可控的内容直接拼接到响应头里,比如:
response.set_header("X-Username", username)而username的值是admin\r\nSet-Cookie: is_admin=1,拼接后响应头里就多了一个伪造的Set-Cookie。如果注入\r\n\r\n,还能直接结束响应头、伪造响应体,演变成响应拆分(Response Splitting)攻击,可能导致缓存投毒、XSS等更严重的后果。
防御手段并不复杂:
- 不要直接把用户输入拼进响应头,使用框架提供的setHeader/appendHeader API,大部分框架会对值做校验或转义。
- 对输入里的
\r、\n做过滤或转义。 - 网关层增加
add_header时的白名单机制,防止头字段被注入。
在排查自家接口时,如果发现异常字符导致网关返回400/502,也要往"响应头里是否出现了非法CRLF"这个方向查一下。有些团队的网关自带安全策略,检测到响应头有异常就直接切断连接,表现就是上游200、下游502。
6.3 从高频场景里捞出的几个实用技能
- JMeter录制HTTPS脚本:JMeter的HTTP(S) Test Script Recorder本质是一个本地HTTP代理,浏览器把请求转发给它,它再转发到目标服务器。录制HTTPS脚本时需要在JMeter生成一个CA证书并在浏览器导入信任。如果录不上,优先检查浏览器代理配置是否生效、CA证书是否被信任。
- WebSocket升级场景:请求头里的
Upgrade: websocket和Connection: Upgrade会触发101状态码,属于协议升级。遇到应用层连接异常,先看状态码是不是卡在101之后,TLS/代理层是否支持升级。 - 大文件下载与断点续传:请求头里的
Range: bytes=0-1023对应响应头的Content-Range与206状态码。a标签下载视频只是冰山一角,真正的下载器都要学会发Range头。
7. 协议细节中的高频坑与自查清单
7.1 我踩过和见过的高频坑
把这几年的经验收敛一下,协议相关的坑高度集中在下面几处。
第一个坑:Content-Length和实际body长度不一致。这种情况多数发生在手动拼接HTTP报文时。解决方法是发送前用字节数组的length而不是字符串的length(),因为中文字符在UTF-8下是3字节,字符串长度会骗人。
第二个坑:把请求头的值塞进URL。签名、token、traceId这类信息放在URL里,容易被访问日志、网关日志、浏览器历史记录泄露。协议层面并没有禁止查询参数,但从规范角度,敏感信息应该走请求头或body。热词里的a标签下载视频请求头怎么带token就是这类矛盾的典型:程序猿知道token不该在URL里,但下载请求又没法自定义请求头。
第三个坑:忽略代理对Connection头的处理。HTTP/1.1里Connection: keep-alive是逐跳头,不应该被转发到下游。很多网关会自动剥离它。如果你在后端代码里读Connection头发现为空,不要惊讶,这是正常行为。
第四个坑:HTTPS证书过期导致服务静默不可用。这个和TLS握手相关——证书过期后,客户端握手报certificate has expired,但服务端日志往往只有一条TLS handshake failed。监控里如果不盯证书剩余有效期,问题往往要到用户投诉才暴露。
7.2 一个可以直接保存的协议自查清单
| 检查项 | 正确姿势 | 常见误区 |
|---|---|---|
| Host头 | HTTP/1.1必须携带 | 以为Host只是可选的、可以被空值替代 |
| Content-Length | 必须与实际body字节数一致 | 用字符串长度代替字节长度 |
| 空行 | 头部与body之间必须有\r\n\r\n | 少一个回车导致整个报文解析失败 |
| 状态码200 | 仅代表HTTP层成功 | 以为业务也一定成功 |
| 304 | 协商缓存命中的正常响应,无body | 以为304是错误 |
| 502 vs 504 | 502=上游响应无效,504=上游超时 | 混为一谈,导致排查方向错误 |
| HTTPS证书 | 需要客户端信任根证书并能校验域名 | 以为装上证书就万事大吉 |
| TLS密钥 | 可用SSLKEYLOGFILE在本地解密抓包 | 以为抓HTTPS一定要装中间人代理 |
| 连接复用 | keep-alive是逐跳头,代理会剥离 | 在后端应用里读不到Connection就认为是bug |
拿这个清单对照你最近处理过的线上故障,大概率能发现,当初绕了半天弯子的问题,其实都落在清单的某一行里。协议知识就是这样,平时不显山不露水,但真遇到问题,它是能帮你把"玄学故障"还原成"确定性事实"的那层基本功。