前几天看一个页面故障,排查群里有人贴了条命令:curl -I 返回 200 OK,于是大家开始怀疑浏览器缓存。可我自己打开页面,依然是报错。更奇怪的是,换成普通的 curl 请求,同一个地址直接返回 500。
这种"同一个地址,一会儿好一会儿坏"的现象,很容易把人带去查网络、代理和缓存。后来发现,两个命令从一开始就没在做同一件事。这篇想用一个很小的例子把它还原出来,也聊聊平时到底该用什么命令验证接口。
一. 看起来都在访问页面,其实问的是两个问题
-
curl -I https://example.com/report里的-I是--head的简写。它会发起 HEAD 请求,只取响应头,不下载响应正文。平时想快速看看状态码、缓存头、跳转地址,这样用很顺手。 -
浏览器打开页面,通常发起的是 GET 请求。服务端要真正生成或读取页面内容,再把正文送回来。对于动态页面,这一步可能访问数据库、查报表数据,甚至调用另一个服务。HEAD 的 200,只能证明这次 HEAD 请求得到了 200,不能替一次 GET 请求作保证。
-
按 HTTP 语义,HEAD 应该像 GET 一样选择资源,只是响应里没有正文;服务端也应该尽量返回相同的响应头。但现实中的应用可能给两个方法写了不同处理逻辑,或者生成正文的环节恰好会失败。看到两个结果不一致时,先确认请求方法,比先猜缓存更省时间。
二. 不用搭业务系统,自己造一个"假正常"页面
- 为了不靠猜,我用 Python 自带的 HTTP 服务写了个小例子。它故意让 HEAD 返回 200,让 GET 返回 500。真实项目里不应该把它当成正常实现;这里就是为了观察"同一个地址、不同方法"的差别。
python
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_HEAD(self):
self.send_response(200)
self.end_headers()
def do_GET(self):
self.send_response(500)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(b"report generation failed\n")
HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()
- 把代码保存为
demo.py,在一个终端运行python3 demo.py,另开一个终端分别请求。第一个命令能看到 200,第二个命令能看到 500 和错误正文。这里的 500 是示例程序故意返回的,不需要去修 Python 或 curl。
bash
curl -I http://127.0.0.1:8765/report
curl -i http://127.0.0.1:8765/report
- 如果想看自己到底发出了什么方法,加上
-v。输出里会出现> HEAD /report或> GET /report。别只盯着最后一行状态码,先确认这次请求的路径和方法跟用户实际操作一致。
bash
curl -v -o /dev/null http://127.0.0.1:8765/report
三. 只看状态码,还可能漏掉正文已经坏了
-
再往前走一步:即使 GET 返回 200,也不能直接等于"页面可用"。有的服务会把错误信息写进正文,状态码仍留在 200;有的页面框架能返回一张空壳,真正的数据请求却失败了。监控检查要回答什么问题,命令就得走到相应的那一层。
-
如果是在检查一个返回 JSON 的接口,我会先用真实的 GET 请求取到响应头和正文,确认状态码、内容类型以及关键字段。对于需要登录的接口,还要带上与业务请求对应的认证信息;否则测到的可能只是登录页或错误页。敏感凭证别直接留在共享终端历史或截图里。
-
curl 默认收到 HTTP 500 时,命令本身不一定以失败状态退出。把它放进脚本做门禁时,可以用
--fail-with-body让 400 及以上状态转成非零退出码,同时保留错误正文方便排查。这个选项解决的是"脚本知道请求失败",不会替你判断正文里的业务结果是不是对的。
bash
curl --fail-with-body -sS -i http://127.0.0.1:8765/report
echo $?
- 还有一个小坑:不要为了"模拟 GET "把
curl -I机械改成curl -I -X GET。-X改的是发出的请求方法字符串,-I仍让 curl 按只收响应头的模式工作,组合起来反而容易把结果看乱。要验证 GET ,直接去掉-I就好。
四. 真碰上这种情况,我会这样缩小范围
-
先拿用户访问的完整地址,用普通
curl -i发一次 GET,把状态码、响应头和正文开头留作现场。若浏览器失败的是某个数据接口,就检查那个接口,而不是只检查首页。地址、查询参数、认证信息和请求方法少一个,都可能测成另一件事。 -
再用
curl -I做对照。如果只有 HEAD 正常,就往服务端按方法分流的逻辑查:应用路由有没有单独处理 HEAD ,反向代理或缓存有没有特殊规则,GET 生成正文时依赖的数据库、文件或下游服务是否出错。不要因为 HEAD 的 200 就先给后端判"没问题"。 -
最后把检查方式改成和目标一致。要判断地址是否存在,HEAD 有它的位置;要判断页面或接口能否实际使用,就走真正的 GET ,必要时检查正文里的关键内容。若目标是浏览器里的完整交互,还得继续看页面发出的后续请求,单条 curl 只能覆盖它实际发出的那一次请求。
五. 先记住这个区别就够了
-
curl -I很方便,但它回答的是"HEAD 请求怎么样"。浏览器打开页面或业务读取接口,问的通常是"GET 请求能不能拿到正确内容"。把这两个问题混在一起,后面的排查方向就容易跑偏。 -
下次看到"探测 200,用户仍报错",我会先把探测命令里的方法、地址和认证条件与真实请求对齐,再看状态码和正文。只改这一点,往往就能把所谓的"玄学故障"还原成一个普通的请求差异。