ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

curl 命令实战:HTTP 请求构造、接口调试与脚本化

curl 命令实战:HTTP 请求构造、接口调试与脚本化

1. curl 到底解决了什么问题

接口联调最尴尬的一幕,是对方甩给你一句"我这边测着没问题",然后就没了下文。你把浏览器打开、地址栏粘上、参数一个个敲回去,结果因为少带了一个请求头,返回的是另一套错误。这种时候,一行能原样复现问题的 curl 命令,价值远超十页文档。curl 是命令行下的网络传输工具,本质做的事情非常朴素:按你的指令拼出一个 HTTP 请求发出去,再把服务器吐回来的东西原样交给你。它不懂业务、不猜语义、不帮你美化数据,正因为这份"笨",它成了排查问题、验证接口、做自动化脚本时最可信的那把尺子。

这篇内容面向三类人:刚接触后端接口、还停留在浏览器点来点去的新手;写了几年业务代码、但对 curl 只停留在curl 网址这一层的开发者;以及需要把网络请求写进部署脚本、定时任务的运维或测试同学。全篇围绕一个目标展开,就是让你在看到任意一条 curl 命令时能读懂它的每一个参数,在需要构造一条命令时能一次写对,在请求出问题时知道从哪个开关下手。所有示例都基于公开保留的示例域名与本地地址,你换成自己的接口即可直接跑。

1.1 一行 curl 命令在真实工作流里的位置

我习惯把 curl 定位成"请求的可执行快照"。浏览器里的操作是不可复现的,你点了几次、中间经过了哪些跳转、带了哪些自动附加的头,很难讲清楚;图形化工具里的配置是存在本地工程里的,发给别人还得导出一个文件。curl 不一样,它就是一段纯文本,可以贴在工单里、写进文档里、塞进 CI 脚本里,还能被版本管理追溯。一份缺陷报告里如果附了一行可执行的 curl,复现成本能从半小时压缩到十秒。

更实际的一点是,curl 处在"最接近协议"的那一层。你看到的响应就是服务器真发出来的东西,没有浏览器帮你解析 HTML、没有前端框架帮你解 JSON、没有工具帮你重定向到别的地址。排查跨域、缓存、压缩、编码、重定向链这类问题时,中间层越少越好判断。我曾经为了确认一个响应到底是服务端没返回某个头,还是前端在某处把它吃掉了,直接用 curl 把原始响应头打出来,两分钟就有了结论。

当然它也有明确的能力边界。curl 不是浏览器,不执行 JavaScript,不做页面渲染,不维护完整的会话状态(除非你显式让它保存 Cookie)。所以抓取动态渲染页面的内容、模拟复杂的登录跳转流程,用它会很别扭。清楚它擅长什么、不擅长什么,比背参数表重要得多。

1.2 几个替代品的取舍对照

工具没有绝对优劣,只有场景匹配度。下面这张表是我在带新人时常用的对照,列的是实际判断依据而不是功能清单。

工具适合场景主要短板我的使用频率
curl接口调试、脚本自动化、问题复现、文件传输无可视化、复杂参数需要记忆每天
浏览器开发者工具观察真实页面发起的请求、复制为命令行难复现、依赖页面操作路径排查前端问题时
图形化 API 调试工具多人协作维护请求集合、环境变量管理命令不便嵌入脚本与流水线中等
wget整站镜像、递归抓取请求构造能力弱于 curl偶尔
语言内置 HTTP 库需要业务逻辑编排、结果解析写一次性验证脚本太重写正式代码时

有个细节值得说:浏览器开发者工具里右键复制出来的请求,本质上就是一条 curl 命令,这说明了 curl 的参数体系已经成为描述 HTTP 请求的通用语法。学会它,等于学会了一门跨工具的"请求方言",很多平台的导入导出都认这个格式。

1.3 版本差异比你想的更重要

在动手之前先跑一次curl --version,这是我养成的第一个习惯。输出里除了版本号,还有一行 features,标明了这个二进制支持哪些协议与能力。很多网上抄来的参数在你的机器上不生效,八成是版本太老。

几个关键的时间节点值得记住:--fail-with-body在 7.76 之后才有,--parallel是 7.66 引入的,--json这个便捷参数要 7.82 以上,--url-query则更晚。如果你在容器镜像里跑脚本,基础镜像自带的 curl 可能比本机老好几年,本地测通、线上报错的情况大多源于此。我的做法是在脚本开头做一次能力探测,缺关键参数就直接失败退出,而不是让它跑到一半给出莫名其妙的结果。

提示:升级 curl 时注意它对 TLS 后端库的依赖,不同后端在证书校验、协议支持上的表现不完全一致。生产脚本里如果需要固定行为,把版本写进依赖清单,别依赖运行环境的默认版本。

2. 请求构造的基本功:URL、方法、头与体

绝大多数 curl 用不明白的问题,根源不在参数多,而在对 HTTP 请求的四个组成部分没有清晰的模型:请求行(方法加路径)、请求头、空行、请求体。curl 的每个参数,本质上都是在往这四块里填东西。脑子里有这张结构图,参数就不是死记硬背,而是"我想改哪一块"。

2.1 URL 里每个字符都在说话

先把最容易被忽略的一件事说清楚:URL 里的查询参数需要编码。空格、中文、&、=、#这些字符出现在值里时,不编码就会把参数结构冲垮。手写编码既费神又易错,--data-urlencode配合-G是更稳的做法,它会把数据编码后拼到查询串上:

curl -G "https://api.example.com/search" \ --data-urlencode "keyword=命令行 工具" \ --data-urlencode "page=1"

请求方法上有个坑得提前说:-d会自动把方法从 GET 改成 POST,-I会改成 HEAD,-T会改成 PUT。所以当你只想加个请求体、但方法必须是 PUT 时,-X PUT是必要的。反过来,如果你用-X POST又加了-L,curl 在跟随 301、302 重定向时会把方法改成 GET,这是 HTTP 规范的行为,不是 bug。真要保留方法和请求体,用--post301、--post302、--post303显式声明。

主机解析也常被拿来做排查手段。当你不确定问题出在 DNS 还是服务本身,--resolve可以绕过解析直接指定 IP,同时保留 Host 头与证书校验所需的域名信息:

curl --resolve api.example.com:443:203.0.113.10 https://api.example.com/health

这招在切换机房、验证新节点、排查解析污染类问题时特别顺手,比改 hosts 文件干净得多,也不会污染本机环境。

2.2 请求体的三条路:-d、--data-binary 与 -F

这三个参数混用是新手最常见的错误来源。它们的区别不在"发不发数据",而在"怎么编码"。

-d会把内容当作表单数据处理,多个-d之间用&拼接,并且会去掉换行。这意味着你用-d @payload.json发一个格式化过的 JSON 文件,里面的换行会被吃掉——多数情况下无害,但如果服务端对原始字节做了签名校验,就会直接失败。--data-binary保留原始字节,不做任何加工,是发文件内容的首选。

发 JSON 目前最省事的写法是--json,它会自动设置Content-Type: application/json和Accept: application/json:

curl --json '{"name":"demo","enabled":true}' https://api.example.com/items

老版本就用显式头加二进制数据,效果一致:

curl -X POST https://api.example.com/items \ -H "Content-Type: application/json" \ --data-binary @payload.json

-F走的是完全不同的路:multipart/form-data。只要涉及文件上传,基本都得用它。它支持给单个字段追加元信息,比如指定文件名和类型:

curl -F "file=@./report.pdf;type=application/pdf;filename=final.pdf" \ -F "note=季度汇总" \ https://api.example.com/upload

注意:-d和-F不要混在同一条命令里。混用会让 curl 以 multipart 为准,之前用-d加的数据可能被静默丢弃,而响应看起来还挺正常,排查起来相当折磨人。

2.3 认证、Cookie 与会话保持

-u user:pass做的是 HTTP Basic 认证,把凭据做 Base64 编码后放进请求头。Base64 不是加密,任何能看到报文的人都能还原出原文,所以这条命令只在 HTTPS 下使用。如果密码里有特殊字符,加引号,或者用-u user让 curl 交互式询问,避免密码进 shell 历史。

需要保持会话时,-c写 Cookie、-b读 Cookie,两个配合就相当于一个极简的会话管理器:

curl -c cookies.txt -X POST https://api.example.com/login \ -H "Content-Type: application/json" \ --data-binary '{"user":"demo","pass":"***"}' curl -b cookies.txt https://api.example.com/profile

Bearer 令牌这类认证本质就是改请求头,-H "Authorization: Bearer $TOKEN"即可。这里提醒一句,令牌别直接写死在脚本里,从环境变量读,并且注意别让-v把完整的认证头打到日志里去。我在一次排查中就把带令牌的调试输出贴进了公共频道,虽然及时发现并轮换了,但那次教训之后,所有调试脚本我都默认加了一层脱敏处理。

3. 调试与输出控制:把黑盒打开

请求发出去没反应,是最让人抓狂的状态。curl 提供的调试手段其实相当丰富,只是很多人只记得一个-v,遇到 TLS 层或者重定向链的问题就卡住了。这一章按"看得多细"来分层讲。

3.1 详细模式的三层:-v、--trace 与抓包之外

-v是最常用的,它把请求头、响应头、连接建立过程打到 stderr,用>和<区分方向。够用,但有个限制:响应体不打,且只给概要。当你需要看完整的字节流、包括那些不可打印字符时,--trace上场:

curl --trace trace.log --trace-time https://api.example.com/health

--trace给的是十六进制加 ASCII 的双栏输出,--trace-ascii只给可打印的文本部分,日志更易读。--trace-time会给每一行加时间戳,这个在分析"卡在哪一步"时非常关键:你能直观看到 DNS 解析花了多久、TLS 握手花了多久、服务器思考了多久才吐第一个字节。有些慢请求根本不是服务端算得慢,而是 TLS 协商反复重试。

-v还有一个隐藏用法是看它到底带了哪些默认头。你会发现 curl 默认加了User-Agent、Accept: */*,如果是 POST 还会加Content-Length或Expect: 100-continue。最后这个头在大请求体时经常惹麻烦,某些老旧网关处理不好会直接挂住,-H "Expect:"把它去掉就能通。

提示:-v的输出走的是 stderr,如果你把 stdout 重定向到文件,终端上照样能看到调试信息,两者互不干扰。这个特性在写脚本时很有用。

3.2 让输出变成可编程的数据:-w 实战

如果说-v是给人看的,那-w就是给程序看的。它能把你关心的指标格式化成一行,方便后续解析入库。常用的变量有%{http_code}、%{time_total}、%{time_starttransfer}、%{size_download}、%{remote_ip}等。

curl -s -o /dev/null \ -w "code=%{http_code} connect=%{time_connect} first_byte=%{time_starttransfer} total=%{time_total}\n" \ https://api.example.com/health

-o /dev/null意味着响应体扔掉,只留指标。做接口健康巡检时,我把这条命令塞进循环,几十个接口的耗时能一次性拉出表格,比逐个打开工具快得多。

指标之间存在勾稽关系,读懂它们能省下大量猜测:time_namelookup是 DNS 耗时,time_connect是 TCP 握手完成的时间点,time_appconnect是 TLS 完成的时间点,time_starttransfer是收到第一个字节的时间点,time_total是全部结束。如果time_connect到time_appconnect差值很大,问题在 TLS 协商;如果time_starttransfer到time_total差值很大,说明服务端在吐数据的过程中卡住了,通常是响应体太大或带宽受限。

4. 文件传输与批量任务的实用配置

curl 最初是以"用 URL 语法传文件"出名的,这块能力到今天依然很能打,只是被 HTTP 调试的光环盖住了。下载大文件、断点续传、限速、批量拉取,几个参数组合起来就是一套轻量的传输方案。

4.1 下载、续传与限速

最省事的下载是-O,用远端文件名存到本地;想指定名字就用-o。如果服务端返回了Content-Disposition,-J能让 curl 用它给出的文件名,三个组合起来是:

curl -OJ https://mirror.example.com/releases/app-1.2.0.tar.gz

-O有个坑必须知道:它取的是 URL 最后一段,如果 URL 带查询串,文件名会变得又长又怪,甚至包含非法字符。稳妥做法永远是显式-o。

断点续传用-C -,让 curl 自己探测本地已有文件的长度并从那个位置接着下。这个参数只在服务端支持 Range 请求时有效,不支持的话 curl 会从头下。判断方法很简单,看-v里服务端有没有返回Accept-Ranges: bytes。

限速用--limit-rate,单位可以是 k、m、g。在共享带宽的机器上跑批量下载,不限速会影响到同机器的其他服务,这是我踩过的坑:一次深夜跑备份脚本,把整台机器的出网带宽占满,导致同机上的监控采集全部超时告警。加上--limit-rate 2m之后风平浪静。

4.2 重试策略与并发

网络抖动导致的失败,靠重试解决最经济。--retry N会在遇到瞬时性错误(超时、连接重置、5xx)时重试,默认还有退避。几个配套参数决定行为细节:--retry-delay固定间隔,--retry-max-time总时长上限,--retry-connrefused把"连接被拒"也算作可重试。

有一个必须提醒的点:默认情况下,curl 只对幂等请求(GET、HEAD、PUT、DELETE 等)重试,非幂等的 POST 不会自动重试,这是保护机制。如果你确定某接口可以安全重试,用--retry-all-errors放开限制,但请先确认服务端做了幂等处理,否则重复下单、重复扣款这类事故就是这么来的。

curl --retry 3 --retry-delay 2 --retry-connrefused \ --connect-timeout 5 --max-time 60 \ -o app.tar.gz -C - https://mirror.example.com/app.tar.gz

并发下载是 7.66 之后才有的能力,-Z开启并行,配合多个 URL 和--parallel-max控制并发度。几十个小文件一起拉的时候,比串行快一个数量级。但要留意并发对目标服务的压力,也留意本地文件描述符上限。

至于上传,-T走 PUT,--upload-file是它的长写法。上传大文件时建议加--progress-bar换一个更紧凑的进度显示,日志里更好看。

5. 把 curl 用进脚本与工程里

命令在终端里敲通只是第一步,真正发挥价值是在脚本和流水线里。这一层关心的就三件事:配置怎么复用、失败怎么判断、输出怎么解析。

5.1 .curlrc 与 -K:把重复参数收起来

每条命令都带一串超时、重试、代理类设置,很快就没人愿意手打了。curl 支持配置文件,默认读家目录下的.curlrc,也可以用-K显式指定。文件里一行一个参数,长参数可以省略前面的两条横线:

# .curlrc connect-timeout = 5 max-time = 60 retry = 2 silent show-error

这里有个细节:默认读取.curlrc意味着你的脚本行为会随开发者的本机配置漂移。写自动化脚本时,我通常用-q作为第一个参数把它关掉,然后用-K加载脚本自己维护的配置文件。这样在任何机器上跑出来的行为都是一致的,也避免了同事本机某个奇怪的配置把流水线搞挂。

配置文件还支持注释和变量式的写法,把常用的几个接口地址、公共头抽出来,脚本主体会干净很多。不过别把它当成密钥仓库,凭据还是走环境变量或者独立的凭据管理方式。

5.2 退出码:脚本判断成败的正确姿势

新手写脚本常见的错误是解析响应内容来判断成功与否,这既脆弱又慢。curl 本身就给了退出码,$?拿到就是结果。常用的几个:0 表示一切正常,6 是域名解析失败,7 是连接失败,28 是超时,35 是 TLS 握手失败,52 是服务端没返回任何数据,56 是接收数据时连接断开。

还有一个特别容易误解的:默认情况下 HTTP 404、500 这类响应,curl 依然返回 0,因为它认为"我成功完成了这次传输,服务器回了内容"。要让非 2xx 变成失败,得加-f。而-f会把响应体也丢掉,看不到服务端的错误详情。7.76 之后的--fail-with-body解决了这个矛盾:状态码非成功时退出码非零,但响应体照样输出。这段在排查第三方接口时救过我很多次。

curl -sS --fail-with-body --max-time 30 \ -o response.json -w "%{http_code}" \ https://api.example.com/orders || { echo "request failed, exit=$?" >&2 cat response.json >&2 exit 1 }

另外-sS这个组合值得养成习惯:-s关掉进度条,-S保留错误信息。只用-s会让真正的报错也消失,排查时一片空白,非常难受。

6. 常见问题与排查实录

参数记得再熟,实际跑起来还是会遇到各种状况。下面这些是我这几年反复遇到、并且每次都有人问的。

6.1 报错速查表

现象/退出码大概率原因首选排查动作
6 无法解析主机域名拼错、DNS 不可用用--resolve绕过解析验证服务本身
7 连接失败端口不通、服务未监听、被网络策略拦截换--connect-timeout缩短等待,确认端口
28 超时连接慢或服务处理慢拆开看time_connect与time_starttransfer
35 TLS 握手失败证书链不全、协议版本不匹配-v看协商细节,--tlsv1.2等显式指定版本
52 空响应服务端直接断开或返回了 0 字节检查服务端日志与网关策略
56 接收失败传输中途连接被重置加--retry,检查中间设备是否有空闲超时
返回 301/302 但拿不到内容没跟随重定向加-L,并用%{num_redirects}确认跳转次数
中文乱码编码不一致检查Content-Type里的 charset,必要时用原始字节日志比对
卡住不动无输出Expect: 100-continue未被正确处理-H "Expect:"去掉该头
上传大文件失败中间层有请求体大小限制与运维确认限制,考虑分片上传

6.2 几个印象深刻的坑

第一个是重定向把方法改掉了。有个接口用 POST 提交,服务端配了 301 跳转,结果-L之后变成了 GET,服务端返回 405,日志里看着像是"方法不对",实际上是 curl 遵循规范的结果。加--post301解决。这类问题不知道规范的人会怀疑人生很久。

第二个是-d @file吃掉了换行。签名校验接口对请求体字节敏感,我把格式化过的 JSON 文件用-d发出去,服务端一直说签名不匹配。排查了快两个小时,最后用--trace-ascii打印原始字节才发现换行被删掉了。换成--data-binary立刻就好。从那以后,凡是涉及签名、哈希、字节级校验的请求体,我一律用--data-binary。

第三个是-k的滥用。跳过证书校验在测试环境确实方便,但很多人把它写进了生产脚本,然后某天证书过期了、被中间设备替换了,脚本照跑不误,问题被完全掩盖。我的做法是只在临时排障时用-k,并且加注释标明原因和有效期,脚本里的默认值永远是严格校验。

第四个是并发下载把目标服务打挂。-Z很爽,但没注意并发的 URL 都指向同一台机器,几十个连接同时建立,触发了对端的限流或者直接把小机器压满。后来给批量任务加了并发上限和启动间隔,稳定得多。这个道理跟写爬虫一样,控制节奏比追求速度重要。

还有一个小技巧分享一下:把常用的请求存成一个纯文本的集合文件,用-K加载,再配合 shell 的循环做批量验证。这比每次重新拼命令行高效得多,也比维护一套图形化工程的导出文件轻量。我现在的做法是每个服务一个.curlrc片段文件,里面放公共头、超时和重试参数,具体请求写在脚本里引用它,接口地址一旦变更只改一处。

最后一句个人体会:curl 的参数多到没人能背全,我到现在也经常--help加 grep 找参数。真正需要记住的不是参数列表,而是那套模型——请求由方法、头部、主体构成,出问题就往这三块和连接过程上找。模型稳了,剩下的事情查文档就行。

返回列表