第 18 章 · HTTP 调试
一句话:在终端里亲手发 HTTP 请求、看清请求和响应每一行的工具(curl、HTTPie、Bruno),加上把本机端口临时开给公网访问的内网穿透工具(cloudflared、ngrok)。 用它还是用别的:一次性的请求、脚本、排错用 curl,哪台机器上都有;参数多、要反复手调的接口用 HTTPie;一组要保存下来、和同事共用的接口请求用 Bruno 存进仓库;网页自己发出的请求先在浏览器开发者工具里看(第 19 章);临时让别人打开本机页面用 cloudflared 快速隧道,要固定地址用 ngrok。 核对版本:curl 8.21(Windows 自带)、HTTPie 3.2、Bruno 4.2、cloudflared 2026.10、ngrok 3,2026-10
18.1 安装与版本
Section titled “18.1 安装与版本”curl 一般不用装:Windows 10 起系统自带 curl.exe,macOS 也自带。Linux 上没有时:
sudo apt install curl想要比系统自带更新的版本,Windows 用 winget install cURL.cURL,macOS 用 brew install curl。查版本,--json 要 7.82 以上:
curl.exe --versioncurl --versionPowerShell 里本章一律写 curl.exe,原因见第 4 章 4.6;bash 里把 curl.exe 换成 curl 即可。
HTTPie 是 Python 写的命令行程序,命令名是 http。Windows 用 uv 装,没有 uv 就 pip install httpie(winget 里的 HTTPie.HTTPie 是它的桌面版,不是命令行):
uv tool install httpiebrew install httpie # macOSsudo apt install httpie # Ubuntu、Debian查版本:http --version。
Bruno 是保存接口请求的桌面程序。Postman 功能相近,要登录账号,集合同步到它的云端,本章不展开:
winget install Bruno.Brunobrew install bruno # macOSsudo snap install bruno # Linuxcloudflared 做快速隧道不需要账号:
winget install Cloudflare.cloudflared# macOSbrew install cloudflared# Ubuntu、Debian:从 GitHub 下载 deb 包再安装curl -LO https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb && sudo dpkg -i cloudflared-linux-amd64.deb查版本:cloudflared --version。ngrok 要先在官网注册账号拿到令牌(authtoken),Windows 从 ngrok 官网下载页下载,macOS 用 brew install ngrok,Linux 按官网下载页加 apt 源;查版本:ngrok version。
18.2 五分钟概念
Section titled “18.2 五分钟概念”- 请求(request):客户端发出的一段文本,四部分:方法(GET 取、POST 新建、PUT 整体替换、PATCH 改一部分、DELETE 删)、URL、请求头(header,一行一个
名字: 值)、正文(body,GET 一般没有)。浏览器、代码里的 HTTP 库、curl 发的都是同一种东西,所以“浏览器能通、代码不通”一定是这四部分里有一处不一样。 - 响应(response):服务器回的一段文本:状态码(status code)、响应头、正文。
- 状态码:三位数,第一位是分组,看第一位就知道该查客户端还是服务器,见下表。
401是没认出你是谁,403是认出来了但没权限,处理方向不同。 - 内容类型(Content-Type):请求头里说明正文是什么格式。
application/json是 JSON;application/x-www-form-urlencoded是name=a&age=3这种表单;multipart/form-data用来上传文件。后端按这个头决定怎么解析正文,头和正文对不上,后端就当成没收到参数。 - 认证头(Authorization):带身份凭据的请求头。
Bearer <令牌>放登录后拿到的令牌(token),大部分 Web API 用它;Basic后面是“用户名:密码”的 Base64 编码,只是编码不是加密,只该在 HTTPS 上用。 - 内网穿透(tunneling):你的电脑在路由器和防火墙后面,外面的人连不进来。隧道客户端从本机主动连到服务商的边缘服务器,服务商给你一个公网 HTTPS 地址,访问这个地址的请求顺着那条连接送回本机端口。本机不用开防火墙、不用动路由器,客户端一停,地址就失效。
curl -v 把一次请求的整个来回都打印出来,每行开头的符号告诉你这一行是什么:
常见状态码按分组:
| 分组 | 意思 | 最常见的三个 |
|---|---|---|
| 2xx | 成功 | 200 成功;201 已创建,POST 新建后返回;204 成功但没有正文 |
| 3xx | 跳转或用缓存 | 301 永久跳转;302 临时跳转;304 没变,用浏览器缓存 |
| 4xx | 请求有问题,改客户端 | 400 请求格式不对,常见于正文不是合法 JSON;401 没带或带错身份凭据;404 路径不存在 |
| 5xx | 服务器出错,查服务端日志 | 500 应用代码抛了异常;502 代理连不上后端;504 后端太久没回 |
502、504 和上传时的 413 在 Nginx 背后怎么查,见第 16 章 16.5、16.6。
18.3 常用命令
Section titled “18.3 常用命令”下面的 curl 命令按 bash 写,PowerShell 里把 curl 换成 curl.exe,其余照抄(PowerShell 7 里单引号包 JSON 能原样传过去,5.1 不行,见 18.6)。URL 里有 & 时整个 URL 加引号。
| 任务 | 命令 | 说明 |
|---|---|---|
| curl:看请求与响应 | ||
| 取正文 | curl <url> |
只打印响应正文 |
| 带上响应头 | curl -i <url> |
状态行和响应头在正文前面 |
| 只要响应头 | curl -I <url> |
发的是 HEAD 请求,个别接口不支持,会返回 405 |
| 看完整来回 | curl -v <url> |
输出在标准错误里,要过滤先加 2>&1 |
| 跟随跳转 | curl -L <url> |
不加时遇到 301、302 就停下,正文是空的或只有一段跳转提示 |
| 存成文件 | curl -o <文件> <url> |
下载用;-O 用 URL 里的文件名 |
| 只看状态码和耗时 | curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" <url> |
输出如 200 0.012s;Windows 的 curl.exe 也认 /dev/null |
| 拆开耗时 | curl -s -o /dev/null -w "dns %{time_namelookup} connect %{time_connect} total %{time_total}\n" <url> |
看慢在域名解析、建连还是等后端 |
| curl:发数据 | ||
| 发 JSON | curl --json '{"name":"a"}' <url> |
自动 POST,并加 Content-Type 和 Accept: application/json |
| 从文件发 JSON | curl --json @body.json <url> |
正文长时用;PowerShell 里写 '@body.json',带引号 |
| 发表单 | curl -d "name=a&age=3" <url> |
自动 POST,类型是 application/x-www-form-urlencoded |
| 上传文件 | curl -F "[email protected]" <url> |
multipart/form-data,file 是后端约定的字段名 |
| 指定方法 | curl -X DELETE <url> |
带 -d、--json 时已经是 POST,不用再写 -X POST;要 PUT 写 -X PUT --json … |
| 加请求头 | curl -H "Accept: application/json" <url> |
可以写多个 -H |
| 带 Cookie | curl -b "session=<值>" <url> |
模拟已登录的浏览器 |
| curl:认证与连接 | ||
| Bearer 令牌 | curl -H "Authorization: Bearer <令牌>" <url> |
令牌会留在命令历史里,用完清掉 |
| Basic 认证 | curl -u <用户>:<密码> <url> |
只写 -u <用户> 会提示输入密码,不进历史 |
| 跳过证书校验 | curl -k https://<地址> |
只在测试时用:等于不验证对方是谁 |
| 信任自签证书 | curl --cacert <根证书.pem> https://<地址> |
比 -k 稳妥,只多信这一个证书 |
| 域名临时指到某个 IP | curl --resolve example.com:443:<IP> https://example.com |
不改 hosts,直接测新服务器或某一台后端 |
| HTTPie | ||
| GET | http GET <url> |
方法可省;本机可简写成 http :3000/users |
| POST JSON | http POST <url> name=a age:=3 |
默认就是 JSON;= 是字符串,:= 是数字、布尔等原样 JSON |
| 发表单 | http -f POST <url> name=a |
类型换成表单 |
| 加请求头 | http <url> "Authorization:Bearer <令牌>" |
名字:值 是请求头,名字==值 是 URL 参数 |
| 只看响应头 | http --headers <url> |
不打印正文 |
| 连请求一起看 | http -v <url> |
先打印发出去的请求,再打印响应 |
| 内网穿透 | ||
| 快速隧道 | cloudflared tunnel --url http://localhost:3000 |
不要账号,打印一个随机的 trycloudflare.com 地址 |
| 保存 ngrok 令牌 | ngrok config add-authtoken <令牌> |
每台机器做一次,写进 ngrok 配置文件 |
| ngrok 隧道 | ngrok http 3000 |
免费账号分到一个固定的开发域名 |
18.4 典型工作流
Section titled “18.4 典型工作流”接口在浏览器里正常,代码里调就报错
Section titled “接口在浏览器里正常,代码里调就报错”后端在 localhost:3000,浏览器里新建用户成功,代码里调同一个接口返回 400。下面按 bash 写,Windows 上在 Git Bash 里做,因为浏览器复制出来的 cURL 命令也是 bash 写法,PowerShell 不认其中的 \ 续行:
- 确认服务在、路径对,先看状态码:
curl -i http://localhost:3000/api/users。 - 按代码的写法发一次,只看发出去的请求行和请求头:
curl -sv -o /dev/null -d '{"name":"a"}' http://localhost:3000/api/users 2>&1 | grep '^>'。 - 输出里是
Content-Type: application/x-www-form-urlencoded:代码没设 JSON 类型,后端按表单解析,读不到字段。换成 JSON 再发,成功就说明问题在这个头:curl -i --json '{"name":"a"}' http://localhost:3000/api/users。 - 类型没问题时,在浏览器开发者工具里把成功的那条请求“复制为 cURL”(第 19 章),粘进终端运行,确认能复现成功。
- 在复制来的命令末尾加上
-sv -o /dev/null 2>&1 | grep '^>',和第 2 步的输出逐行对比Content-Type、Authorization、Cookie。 - 浏览器那边多了
Cookie或Authorization,就是代码没带登录凭据;带上令牌再试:curl -i -H "Authorization: Bearer <令牌>" --json '{"name":"a"}' http://localhost:3000/api/users。 - 请求对了但很慢,看时间花在建连还是等后端:
curl -s -o /dev/null -w "connect %{time_connect} total %{time_total}\n" http://localhost:3000/api/users。
把本机正在开发的网页临时给别人看
Section titled “把本机正在开发的网页临时给别人看”下面按 PowerShell 写,bash 里把 curl.exe 换成 curl。
- 起本机服务,这里用 Python 自带的文件服务代替你的开发服务器:
python -m http.server 3000。 - 另开一个终端,确认本机能访问:
curl.exe -I http://localhost:3000。 - 起快速隧道,几秒后输出里出现
https://<随机词>.trycloudflare.com:cloudflared tunnel --url http://localhost:3000。 - 从本机走公网地址访问一次,返回
200,第 1 步的终端里多一行访问日志:curl.exe -I https://<随机词>.trycloudflare.com。 - 把地址发给同事。看完在隧道终端按
Ctrl+C,再访问就打不开了:curl.exe -I https://<随机词>.trycloudflare.com。 - 下次启动是另一个随机地址。要固定地址就用 ngrok,先保存令牌:
ngrok config add-authtoken <令牌>。 - 起隧道,终端里的
Forwarding一行就是公网地址:ngrok http 3000。
图里箭头 ⓪ 是关键:连接是本机连出去的,所以不用在路由器上做端口映射。反过来,拿到地址的任何人都能访问这个端口,别对着有真实数据的服务开。
用 Vite 开发服务器时,外面打开页面看到 Blocked request. This host (…) is not allowed.,在 vite.config.js 的 server.allowedHosts 里加 .trycloudflare.com。
在 Bruno 里存一组接口请求,提交到仓库
Section titled “在 Bruno 里存一组接口请求,提交到仓库”Bruno 的集合(collection)就是一个目录,每个请求一个纯文本文件(.bru,3.0 起也可选 YAML),能像代码一样 diff、评审、合并:
- 在 Bruno 里点 Create Collection,名字填
shop-api,位置选项目仓库的根目录。 - 新建请求,URL 写
{{baseUrl}}/api/users,按Ctrl+S保存。 - 在 Environments 里新建
local,变量baseUrl填http://localhost:3000;令牌变量勾上 Secret,值只存在本机,不写进文件。 - 回到终端看 Bruno 生成了哪些文件:
git status。 - 提交整个集合目录:
git add shop-api。 - 写提交说明:
git commit -m "添加用户接口的 Bruno 集合"。 - 同事
git pull后,在 Bruno 里点 Open Collection 选这个目录,填上自己的令牌就能发。 - 想在命令行或 CI 里跑整个集合,装 Bruno 的命令行:
npm install -g @usebruno/cli。 - 进入集合目录,用
local环境依次发出全部请求:bru run --env local。
18.5 配置要点
Section titled “18.5 配置要点”| 配置项 | Linux | macOS | Windows |
|---|---|---|---|
| curl 默认参数 | ~/.curlrc |
~/.curlrc |
%USERPROFILE%\.curlrc(也认 _curlrc) |
| HTTPie 配置 | ~/.config/httpie/config.json |
~/.config/httpie/config.json |
%APPDATA%\httpie\config.json |
| Bruno 集合 | 集合目录里的 bruno.json(YAML 格式为 opencollection.yml)和请求文件 |
同左 | 同左 |
| Bruno 环境 | 集合目录下 environments/<环境名>.bru 或 .yml |
同左 | 同左 |
| cloudflared | ~/.cloudflared/,命名隧道的凭据和 config.yml 在这里 |
同左 | %USERPROFILE%\.cloudflared\ |
| ngrok | ~/.config/ngrok/ngrok.yml |
~/Library/Application Support/ngrok/ngrok.yml |
%LOCALAPPDATA%\ngrok\ngrok.yml |
.curlrc 对每一条 curl 命令生效,包括别人写的脚本,所以别放 -H、-d 这类改变请求内容的选项;临时不想读它,在命令最前面加 -q。HTTPie 的 config.json 用 default_options 列表写默认参数;在脚本、定时任务里调用 http 时把 --ignore-stdin 放进去,见 18.6。快速隧道不需要配置文件,固定域名的命名隧道才在 .cloudflared 里放 config.yml;ngrok config check 打印 ngrok 实际读的是哪个文件。
一个最小的 .curlrc,一行一个选项,长选项去掉前面的 --:
# 自动跟随 301、302 跳转,等于每条命令都加了 -Llocation# 最多跟随 5 次跳转,防止两个地址来回跳max-redirs = 5# 10 秒连不上就放弃,不干等connect-timeout = 10# 加了 -s 静默时,出错照样打印原因show-error18.6 注意事项
Section titled “18.6 注意事项”- Windows PowerShell 5.1 里
curl -I <url>报缺少参数 Uri,或停下来让你输入 Uri。 原因:5.1 的curl是Invoke-WebRequest的别名(第 4 章 4.6)。处理:一律写curl.exe,或改用 PowerShell 7。 - PowerShell 里发 JSON,后端收到
{name:a};或 URL 带&时报“术语 ‘b=2’ 不会被识别为 cmdlet…”。 原因:5.1 吃掉参数里的双引号(同第 5 章 5.6);&是后台运行符,URL 在此截断。处理:JSON 在 7 里用单引号'{"name":"a"}'或--json '@body.json';URL 整个加引号。 curl: (60) SSL certificate problem: self-signed certificate,Windows 上是schannel: SEC_E_UNTRUSTED_ROOT。 原因:对方证书是自签的或由公司内部 CA 签发,curl 不信任。处理:--cacert <根证书.pem>指定根证书;本机测试可临时加-k,不要写进脚本。- 报
curl: (7) Failed to connect to localhost …,连不上服务器。 原因:服务没起、端口写错,或服务只监听在另一个地址(如容器里)。处理:先看服务的终端日志确认端口,再curl -v看它实际连的是哪个 IP 和端口。 - 用
-d发 JSON,后端返回400或字段全是空的。 原因:-d默认带Content-Type: application/x-www-form-urlencoded,后端不按 JSON 解析。处理:改用--json,旧版 curl 加-H "Content-Type: application/json"。 curl -i只看到301或302和一个Location,正文是空的或只有一段跳转提示。 原因:curl 默认不跟随跳转。处理:加-L;要看跳了几次、跳去哪,用-L -v。- 隧道地址昨天还能用,今天打不开。 原因:快速隧道的地址只在
cloudflared运行时有效,每次启动都换一个。处理:重新运行cloudflared tunnel --url http://localhost:3000,把新地址发给对方;要固定地址用 ngrok。 - 脚本或定时任务里的
http命令一直卡住不返回。 原因:标准输入不是终端时,HTTPie 以为正文要从标准输入读,一直在等。处理:加--ignore-stdin,或写进 HTTPie 配置的default_options。
18.7 深入阅读
Section titled “18.7 深入阅读”- curl 手册:curl 全部选项的说明,查 –json、-w 能打印的变量、.curlrc 的查找顺序。
- Everything curl:curl 作者写的完整教程,从发 GET 讲到认证、Cookie、代理和 TLS 证书。
- HTTPie 文档:请求项写法(= 和 := 的区别)、表单和上传、会话、配置文件和 –ignore-stdin。
- Bruno 文档:集合与请求文件格式、环境变量和 Secret 变量、Git 协作、命令行 bru run。
- Cloudflare 快速隧道:cloudflared tunnel –url 的用法、只许指定邮箱访问的写法和快速隧道的限制。
- HTTP 响应状态码(MDN):每个状态码的含义和典型场景,遇到不认识的状态码来这里查。