
我先说一个比较反直觉的结论HTTP协议从来不是“会发请求、会看Network面板”就等于掌握了。我见过太多人把几十个状态码背得滚瓜烂熟可真到线上出问题——浏览器访问正常、脚本调用就401状态码明明是200但页面内容被截断HTTPS流量在Wireshark里永远是一堆不可读的密文——立刻就没方向了。这篇博文我打算把HTTP/HTTPS协议最核心的四块骨架一次讲透请求头、响应头、状态码、数据包结构。不管你是前端、后端、测试、运维还是安全方向只要想把协议底层逻辑真正打通这篇都值得花二十分钟读完。内容全部来自我这些年实际排查问题、抓包调试时的经验总结不写教科书式的废话。1. 为什么我建议你把HTTP协议从头再学一遍1.1 会调接口不等于懂协议从一次“假502”说起有次线上报警某个微服务接口大面积返回502 Bad Gateway。按直觉先查应用日志服务进程明明活着既没OOM也没panic日志里一条错误都看不到再查下游数据库、Redis指标全部正常最后查网关层发现是上游一个接口返回的响应体超过了2MB而网关设置的proxy_read_timeout只有10秒。响应体太大客户端在慢速网络上读取超时网关等不到完整的响应就直接掐断连接于是客户端统一收到502。这就是典型的“假502”服务本身没挂是网关与上游之间的数据传输超出了超时窗口。如果只盯着“502是网关错误”这个标签你会在应用日志里白白耗掉半天。反过来如果当时能直接从数据包层面看到TCP流在哪一秒断开、连接是否被RST五分钟就能定位到“超时”这个根因。我讲这个例子的意思是HTTP协议不是一个需要“背诵”的面试题集而是一套排查问题时的思维框架。状态码告诉你“结果大概是什么类型”响应头告诉你“这个结果到底发生了什么”数据包结构告诉你“双方在传输层是怎么交互的”。这三者缺一不可。1.2 别把“记住报文格式”和“理解协议语义”混为一谈很多人入门HTTP时第一件事就是背GET和POST的区别背请求头有哪些字段。这些算“报文格式”但它只是协议最表层的部分。HTTP真正的难点也是真正值钱的部分是“语义”——每个头部字段、每个状态码背后代表的意义以及两端如何根据这些语义做出不同行为。打个比方。HTTP报文就像寄快递起始行是快递单号请求头是面单上的收发件人、保价声明、配送说明请求体响应体是包裹里的实际物品。一个快递员如果只看得懂单号但看不懂“易碎品”“生鲜优先派送”这些标注那他永远只能是个搬运工。协议排查也一样如果你只看得懂“返回200了”但看不懂Cache-Control: no-store意味着什么、Content-Type决定浏览器是不是要下载文件那排查效率就永远上不去。我推荐把HTTP学习拆成最小闭环请求报文、响应报文、头部语义、状态码语义。本篇就是围绕这四块展开的。1.3 这些天天见到的报错本质都是协议问题我自己平时会刷很多技术社区发现经常有人把一堆报错贴出来求助比如unexpected status 502 bad gateway: unknown error——这是典型的网关层错误upstream returned http 403 forbidden——上游明确拒绝了这次请求unavailableinvalidchannel: http 403 forbidden for channel anaconda/pkgs/main——软件源请求被服务端策略拦截api call failed after 3 retries: http 500: llama-server process has terminated——服务端进程异常导致内部错误start http 524——这是网关等待上游超时的专有状态码。这些报错如果只看表面文字会以为是一次次独立的环境问题。但往底层看全都是“客户端把一个请求按某种结构发给服务端服务端按某种语义处理完返回了一个代表结果的状态码”——你在排查时缺的不是重试次数而是对报文结构和状态码语义的敏感度。2. 请求头从一次真实请求的报文解剖开始2.1 一份能被逐行读懂的HTTP请求报文先不要看理论我们直接看一份真实的HTTP请求报文。执行curl -v https://example.com/api/health抓到的原始内容大致是GET /api/health HTTP/1.1 Host: example.com User-Agent: curl/8.0.1 Accept: */* Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q0.9 Connection: keep-alive Cookie: sessionidabc123; themedark第一行叫请求行由三部分组成方法GET、请求URI/api/health、协议版本HTTP/1.1。从第二行开始到空行之前都是请求头。空行之后的内容叫请求体GET方法通常没有请求体POST/PUT/PATCH才会携带。初学者最容易犯的错是把请求头和请求体搞混。请求头是“对这次请求的说明”请求体才是“真正给服务端的数据”。比如Content-Type说明请求体是什么格式Content-Length说明请求体有多长Authorization说明“我是谁、有没有权限”。服务端拿到请求后会先解析请求头再根据头的描述去解析请求体。2.2 高频请求头字段职责清单我直接把日常开发里最常打交道的请求头整理成了一张清单每个都写清了作用和一个常见坑请求头作用常见坑Host指定目标主机和端口虚拟主机就靠它路由HTTP/1.1起必须携带缺失或错误会收到400Content-Type说明请求体的媒体类型前后端格式不一致时服务端会直接415Content-Length请求体的字节长度手写协议时算错会导致对端一直等待Transfer-Encoding值为chunked时分块传输无需Content-Length和Content-Length同时出现会引发安全问题Authorization携带凭证如Bearer token、Basic base64串放在Header里比放在URL里安全但浏览器有限制Cookie携带会话状态容易被CSRF利用属性和作用域极其关键User-Agent声明客户端类型和版本反爬和浏览器兼容性判断的重要依据Referer来源页面地址鉴权别完全依赖它可被篡改Origin跨域请求时标明来源CORS服务端校验的主要依据比Referer更可靠Accept期望服务端返回的媒体类型忘记设置可能在拿到XML时一脸懵Accept-Encoding支持的压缩算法服务端返回的结果可能是gzip压缩后的二进制Range请求资源的字节范围用于断点续传服务端支持返回206不支持返回200全文Connection管理连接策略HTTP/1.1默认为keep-alive设置close会让每个请求重新建立TCP连接显著增加时延X-Forwarded-For经过代理时记录原始客户端IP是个可伪造的头不能完全信任2.3 三个最容易踩坑的请求头场景第一个坑是Content-Type与实际请求体不匹配。比如前端用fetch发送JSON对象但忘记设置Content-Type: application/json浏览器默认会以text/plain;charsetUTF-8发送后端框架解析不到JSON字段返回400或415。排查时先看请求体是什么格式再看请求头声明的格式两边对不上问题基本就出在这。第二个坑是代理环境下的Host与转发头。现在很多系统前面都挂了Nginx、网关或者CDN服务端看到的Host、X-Forwarded-For、X-Real-IP往往不是真实客户端的值。所以两边要约定清楚服务端在做域名白名单、IP白名单校验时一定要从网关固定写入的头里取值而不是直接信任客户端传来的X-Forwarded-For。安全测试里常见的手法就是手动改这个头来绕过IP限制这个思路本身没错但也反向提醒服务端这个头必须由可信代理覆盖不能裸信。第三个坑是想要在浏览器下载时带Token。很多人问“a标签下载视频请求头怎么带token”其实是混淆了场景a标签发起的导航请求由浏览器控制你没法自定义Header。解决方案无非两种一是如果服务端允许把token拼在URL query里/file?id1tokenxxx但token容易出现在日志和访问记录中二是用fetch发起请求并设置Authorization头拿到响应后转成Blob再用URL.createObjectURL生成一个临时地址给a标签触发下载。后者虽然多写几行代码但不会把token暴露在地址栏。同时另一个好习惯是给Content-Disposition设置attachment文件名时注意做URL编码否则会遇到中文文件名乱码。3. 响应头决定你“看到什么”的另一半协议3.1 同样一个200内容为什么千差万别请求发出去之后服务端返回的响应报文结构是状态行、响应头、空行、响应体。状态行就是HTTP/1.1 200 OK这样的内容从第二行开始叫响应头。响应头里最容易被忽略但又最影响体验的是Content-Type。你访问一个接口服务端返回Content-Type: application/json浏览器就会把响应体当JSON解析如果返回text/html浏览器会直接渲染成页面如果返回application/octet-stream浏览器就倾向下载。很多前端的“乱码”“弹窗下载”问题本质都是服务端响应头里Content-Type的charset或者媒体类型写错了。比如application/json; charsetutf-8和application/json在浏览器行为上没本质区别但如果JSON里含中文且响应头没指定字符集部分老版本浏览器可能会按系统编码解析导致中文乱码。这类问题从浏览器Network面板里点开响应头一眼就能看到。再说Content-Encoding。很多服务端默认开启gzip压缩响应头里会看到Content-Encoding: gzip响应体是压缩后的二进制内容。浏览器会自动解压所以你看不到差异但如果你用脚本去读原始响应拿到的是压缩流不解压就是乱码。同理Accept-Encoding头里如果声明支持br服务端可能返回Brotli压缩解析的时候都要配套处理。3.2 缓存三件套与304状态码的真面目响应头里和性能关系最大的就是缓存控制。核心三件套是Cache-Control、Expires、ETag。先理解一个框架浏览器缓存分为强缓存和协商缓存。强缓存阶段浏览器在有效期内直接使用本地副本不发请求。这个“有效期”由Cache-Control: max-age3600或老旧的Expires绝对时间HTTP/1.0遗留决定。一旦强缓存失效浏览器会带着If-None-Match值是之前响应里的ETag或If-Modified-Since值是之前响应里的Last-Modified去问服务端资源有没有变。服务端对比后如果没变直接返回304 Not Modified不带响应体浏览器继续用本地缓存。所以304不是一个错误它是协商缓存命中的标准响应。很多新手在Network里看到304就以为资源出问题了其实这正好说明服务端和浏览器配合得很健康。反之如果服务端每个静态资源都返回200并且带着很大的响应体说明缓存策略完全没有配置到位。排查时看一眼响应头里有没有ETag和Cache-Control基本就能判断缓存链是否完整。3.3 Set-Cookie 与安全响应头Set-Cookie是服务端给客户端种会话状态的主要途径也是安全攻击的高发区。它有几个属性必须吃透HttpOnly禁止JavaScript读取Cookie防止XSS把凭证偷走SecureCookie只在HTTPS连接上传输防止明文网络中被截获SameSiteLax|Strict|None控制跨站请求是否携带Cookie是CSRF防护的重要防线。None必须配合Secure使用否则现代浏览器会直接拒绝Domain和Path控制Cookie的作用范围。响应头里的安全项还有几个大佬Content-Security-PolicyCSP用来限制页面能加载哪些来源的资源是XSS最硬的一道防线X-Frame-Options用来防止页面被嵌套进iframe防点击劫持Strict-Transport-SecurityHSTS告诉浏览器“以后只准用HTTPS访问本站”可以自动把http请求升级为https请求。我处理过几次线上安全扫描整改项几乎都是这些响应头没配好。老实说配置它们成本极低收益极高建议大家在做安全基线时直接按这个清单检查一遍。3.4 Content-Disposition文件下载的文件名乱码问题响应头里还有一个经常在“文件下载”场景被用到Content-Disposition: attachment; filenamereport.pdf。加了它浏览器会触发下载而不是尝试渲染。文件名如果是中文直接放进filename报告.pdf很容易乱码正确的做法是用filename*UTF-8%E6%8A%A5%E5%91%8A.pdf这种RFC 5987格式。有些老版本浏览器对filename*支持不好可以同时提供filename作为兜底。另外如果你既想用a标签下载又要带token我前面讲过用fetch转Blob的方案。这个方案中更需要关注的恰恰是后端响应头比如Content-Disposition里的文件名你要么通过接口额外返回文件名字段要么去解析响应头里的filename*。我自己踩过几次坑都是卡在文件名编码上所以这块多说两句。4. 状态码不是背表是学会“看码猜因”4.1 五个区间的语义逻辑状态码不是让你一个个背的而是帮你建立一个“看区间猜问题”的直觉区间语义常见子集1xx信息性响应表示对方正在处理或协议切换100 Continue101 Switching ProtocolsWebSocket常用2xx请求成功200 OK201 Created204 No Content206 Partial Content3xx重定向告诉客户端“你要的东西在别处”301/302/303/307/308304 Not Modified4xx客户端错误问题出在你的请求上400/401/403/404/405/408/409/413/415/4295xx服务端错误问题出在服务器上500/501/502/503/504/505这个直觉非常重要收到4xx你第一反应应该是“我这次请求哪里有问题”而不是盲目重试收到5xx你才应该去检查服务端日志和负载情况。很多人一遇到错误就重启服务、重试请求其实是把4xx当5xx处理方向完全反了。4.2 高频状态码逐个实战我整理了一个高频状态码排查速查表实际工作中照着看足够用状态码含义排查思路200成功但注意业务错误也可能返回200需要看响应体里的业务码201创建成功通常是POST/PUT创建资源后的标准响应204无内容通常用于DELETE成功响应体为空206部分内容Range断点续传、视频拖动进度时常见不算错误301永久重定向注意浏览器和搜索引擎会缓存改完之后要清缓存302临时重定向很多登录跳转用302但语义上和303/307有细微差别303See Other明确告诉客户端用GET访问另一个URL常用于POST后跳转304协商缓存命中配合ETag/Last-Modified使用没有响应体307临时重定向保持方法重定向后仍保持POST和302行为差异很大308永久重定向保持方法301保持方法版本400请求语法错误优先查请求头是否完整、Content-Type是否匹配、参数格式是否合法401未认证没有提供token或token失效检查Authorization头403无权限已认证但权限不够检查用户角色、IP白名单、防盗链404资源不存在检查URL路径、服务路由配置、网关转发规则405方法不允许比如只允许POST的接口你发了GET检查路由方法限制408请求超时客户端发送请求速度太慢或请求体长时间没发完409冲突常见于并发创建同名资源、版本冲突413请求体过大检查上传大小限制、网关client_max_body_size415不支持的媒体类型Content-Type服务端不认416Range范围不合法请求Range超出文件大小429请求过多触发限流检查是否有并发循环请求等退避窗口500服务端内部错误看应用日志、进程状态、异常栈501未实现服务端不支持这个请求方法502网关/上游无效响应检查网关与上游的连接状态、上游进程、超时配置503服务不可用常见于服务启动中、过载保护、CPU爆满504网关超时上游处理时间超过网关超时时间查慢SQL、死循环、锁等待505HTTP版本不支持很少见通常是协议解析库版本过老4.3 三组最容易混淆的状态码对比第一组301/302/303/307/308。这里的水最深。HTTP/1.1时代302语义很模糊很多服务端在POST后返回302浏览器会擅自把POST改成GET并丢失请求体。后来规范明确区分了307临时重定向保持请求方法和body和308永久重定向保持请求方法和body。现在如果你设计的接口要在重定向后保留POST语义应该用307或308如果希望POST后跳到GET页面用303更规范。电商支付后跳转、OAuth授权码回调这里全是坑。第二组401 vs 403。401是“你没有认证”意味着你还没证明你是谁403是“你是你但没有权限做这事”。打个比方401是门卫不知道你身份让你出示工牌403是门卫查了你工牌发现你没权限进这个区域。排查时如果收到401先看Authorization头有没有正确带token收到403则看当前账号的角色和权限配置再看有没有IP白名单拦截。第三组500/502/503/504。500是应用层自己抛异常502是网关拿到了一个无效的响应通常上游连接被重置或上游没起来503是服务还在但主动拒绝过载、维护504是网关把请求转发给了上游但上游迟迟没在超时时间内返回。定位思路500去查应用日志502查上游进程和连接数503查负载和部署状态504查慢接口和数据库。4.4 用状态码快速定位“热搜报错”我拿几个网上常见的报错做一次实战演示。第一个是unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:1572。看到502第一步不应该是怀疑应用逻辑而是看网关进程。这个报错发生在访问本机端口说明是一个本地代理进程转发到127.0.0.1:1572时出了岔子。用ss -lntp看这个端口在不在监听进程是否存活如果监听正常再用curl直接访问该端口绕过上层代理很快就能判断是上游服务本身的问题还是代理配置的问题。这类“套娃式”代理结构状态码唯一能告诉你的就是“那层负责转发的没拿到好结果”。第二个是unexpected status 404 not found。404是最容易被误解的状态码。它不一定代表服务器上没有这个文件还可能是网关路由没匹配上、反向代理的location路径配错了、或者服务端框架没有为这个URI注册路由。排查时用curl -I直接访问目标地址看返回404的是网关层还是应用层——响应头里Server字段和Via字段能帮你分辨。第三个是http 500: llama-server process has terminated。这是应用进程自己挂了需要去看进程崩溃日志比如core dump、OOM、段错误。状态码500会把你指向应用层至于具体是哪个异常状态码帮不了你得靠日志和监控。但反过来如果你看到的是502就不能只看这个进程而是要连着它上游的所有环节一起查。第四个是start http 524。524其实是Cloudflare平台特有的状态码表示“源站已经在规定时间内建立TCP连接但始终没有返回HTTP响应”本质上和504同族。排查时重点看源站请求处理耗时、是否有长时间挂起的请求、PHP-FPM/Java线程池是否耗尽。5. 数据包结构在抓包工具里看懂HTTP5.1 HTTP报文的四件套结构理解数据包结构先从最原始的报文开始。一个完整的HTTP请求报文是这种“四件套”结构起始行 头部字段 空行 请求体可选空行是严格要求的它用\r\n标识头部的结束。很多手写HTTP协议的人最容易犯的错就是忘记在头部结束后加一个空行结果服务端一直在等待头部结束最终返回400或408。响应报文同理格式是状态行响应头空行响应体。这里要给一个很实用的理解方式看TCP流而不是只看UI里的表格。在Wireshark里选择一条HTTP请求右键“Follow TCP Stream”你看到的就是协议最原始的报文样子。你会看到请求行、请求头、空行、请求体然后是状态行、响应头、空行、响应体清清楚楚。养成看原始报文的习惯很多“框架封装得太好导致你不知道底层发生什么”的问题会豁然开朗。5.2 Wireshark抓HTTP包的基本姿势Wireshark是排查协议问题的王炸工具但很多人第一眼就被它的复杂界面劝退了。其实日常HTTP排查只需要掌握几个基础操作抓包过滤器tcp port 80只抓HTTP流量tcp port 443只抓HTTPS流量。如果你只关心某个IP加host 1.2.3.4显示过滤器在顶部过滤栏输入httpWireshark会自动只显示HTTP协议解析后的数据包。输入http.request看请求输入http.response看响应输入http.response.code 404可以专门过滤404响应跟踪流右键任意HTTP包选“Follow”-“TCP Stream”直接看完整的请求和响应原文统计功能菜单“Statistics”-“HTTP”-“Requests”能看到每个URI的请求次数、平均响应时长、状态码分布用来快速定位“哪个接口在频繁报错”非常方便。抓包时要注意如果没用代理且流量是HTTP明文Wireshark直接就能看到报文但如果流量是HTTPS加密的Wireshark默认只能看到TCP层信息看不到HTTP内容。这个问题下面HTTPS章节继续讲。5.3 用数据包分析“偶发超时”与“连接复用”我曾经排查过一个问题客户端调用接口大部分时候50ms返回偶尔会卡在5秒才返回。从应用日志里看不出规律但抓包后立刻暴露了真相——有些请求经过了完整的TCP四次握手说明没有复用连接每个新连接都要重新走一遍TCP握手和TLS握手光是握手就花了几百毫秒再加上网络波动就造成了偶发超时。抓包看这个流程你会看到这样的结构TCP 三次握手 TLS ClientHello / ServerHello / Finished如果是HTTPS HTTP 请求行 请求头 请求体 HTTP 状态行 响应头 响应体 TCP 四次挥手如果客户端设置了Connection: keep-aliveHTTP/1.1默认HTTP请求会复用在同一个TCP连接上整个连接上会有多个请求/响应交替排列中间没有新的握手包。这个“多个HTTP包挤在一个TCP流里”的现象就是连接复用keep-alive。排查“偶发超时”时你重点关注同一个TCP流里有没有频繁关闭和重开连接如果有就得去调整客户端的连接池大小、空闲超时时间或者检查服务端的Keep-Alive超时配置。6. HTTPS协议栈多出来的一层“加密皮”6.1 HTTP与HTTPS的本质差异HTTPS不是一套新协议它的全称是“HTTP over TLS”——在HTTP和TCP之间插入了一层TLS协议。TLS负责做三件事加密传输内容、校验服务器身份、保证传输过程不被篡改。正因如此HTTPS的默认端口从80变成了443URL scheme从http变成了https。但要注意TLS是在TCP之上、HTTP之下。TCP负责建立可靠连接TLS负责在这个连接上再建立一层加密通道HTTP报文只是这个加密通道里传输的普通数据。这个层次关系极端重要因为它解释了一个事实HTTPS不是对所有数据都加密的。IP层、TCP层的元数据源IP、目标IP、端口号、数据包长度、TLS握手时的证书和SNI在网络上仍然是可见的加密的只是HTTP报文本身。所以HTTPS并不能完全隐藏“你在访问哪个网站”只是隐藏了“你在这个网站上看了什么、发了什么内容”。6.2 TLS 1.2握手的关键步骤我讲一下目前兼容性最广的TLS 1.2握手流程理解它你就能明白为什么HTTPS抓包需要“中间人”或者“密钥导出”。ClientHello客户端向服务端发送一个明文消息包含TLS版本、支持的加密套件列表、一个随机数Client Random、以及SNIServer Name Indication也就是你访问的域名。ServerHello服务端从列表里选一个加密套件附上自己的随机数Server Random并下发自己的数字证书Certificate。证书里包含公钥和服务器身份信息。证书校验客户端校验证书链是否由受信任的根证书签发、证书域名是否匹配、是否过期。校验失败会警告“不安全连接”。密钥交换客户端根据加密套件生成一个随机数Pre-Master Secret用服务端公钥加密后发送给服务端或通过ECDHE等算法实现前向保密。双方用这三个随机数各自计算出对称加密的主密钥Master Secret。Finished双方互相发送Finished消息确认协商完成。之后所有HTTP数据都用对称加密传输。这里最高频的坑是证书过期或者证书链不完整。证书链不完整时服务端只发了叶子证书没有中间证书客户端在系统根证书库里找不到可信任的上级就会连接失败。排查这类问题可以用openssl s_client -connect domain:443 -showcerts能看到服务端下发的完整证书链非常直观。6.3 为什么能抓HTTPS明文中间人代理和根证书很多人一听到“HTTPS加密不能抓包”马上就慌。其实无论是Fiddler、Charles、Burp Suite还是JMeter录制HTTPS脚本核心用的原理都一样中间人代理MITM。步骤大致是客户端把流量代理指向抓包工具抓包工具与真实服务器建立一条TLS连接跟服务器正常完成握手抓包工具再给客户端颁发一张由它自己私钥签名的伪造证书客户端提前把抓包工具的根证书安装进系统信任库所以会信任这把伪造证书于是客户端与抓包工具之间、抓包工具与服务器之间分别有两条TLS连接。抓包工具站在中间两边都能解密、读取明文HTTP内容。所以Burp Suite抓HTTPS前要求你先安装其CA证书JMeter录制HTTPS脚本时要在HTTP代理服务器配置里导入它的证书根因都在这里。它依赖的是“客户端是否信任这把CA根证书”的机制。另一种不需要中间人的方式是SSLKEYLOGFILE。在浏览器环境变量里设置SSLKEYLOGFILE/path/to/keys.log浏览器会把每次TLS握手的对称密钥种子写入这个文件。Wireshark里设置“Edit”-“Preferences”-“Protocols”-“TLS”-“(Pre)-Master-Secret log filename”指向这个文件它就能直接解密HTTPS流量。这个方式对浏览器内部发起的请求特别好使缺点是需要控制浏览器启动环境。6.4 HTTPS抓包里常见的拦路虎我在实际抓包中踩过的坑总结起来就这几类第一类是证书信任问题。手机安装代理证书后App仍然报了CERTIFICATE_VERIFY_FAILED原因是很多App把“证书校验”做在代码里只信任预置证书不鸟系统信任库。这时候要么用可以刷入系统证书的测试机需要root要么用Frida/objection等工具绕过证书锁定。对于webview场景可以尝试将证书安装到系统证书目录但要分Android版本处理。第二类是密钥日志文件失效。设置了SSLKEYLOGFILE但Wireshark解不开往往是因为浏览器用的是TLS 1.3下的ECDHE而日志文件路径没读对或者浏览器是运行在后台服务/其他用户下环境变量没生效。建议先看日志文件有没有持续写入没写入就去检查浏览器的启动环境变量。第三类是连接复用导致的过滤误判。一个TCP流里既有HTTP请求又有HTTPS握手过滤表达式写得太宽会把握手阶段也包进来。习惯用http2和tls.handshake.type来区分而不是只看端口。最后再分享一个我自己的习惯抓包这个动作我很少一上来就开Wireshark。我的顺序是先看状态码判断是4xx还是5xx缩小范围再从头或响应头里找“为什么”——是谁返回的、有没有缓存、有没有重定向、Cookie有没有种进去最后才上抓包工具看原始报文确认TCP层和TLS层有没有捣乱。这个顺序帮我解决过非常多的线上问题比漫无目的地抓包高效得多。另外有个小技巧遇到不认识的响应头别急着忽略右键复制出来搜一下很多坑的线索都藏在一些“莫名其妙”的头字段里。比如Server: nginx/1.18.0能告诉你网关软件Via: 1.1 cdn能告诉你经过了CDNX-Cache: HIT能告诉你缓存有没有命中Alt-Svc则暗示服务端支持HTTP/3。多看多拆协议能力就是这么一点点积累起来的。