第一次在服务器上敲 curl,我猜大部分人的经历都差不多:照着某篇教程复制了一行curl -fsSL https://xxx/install.sh | sh,回车,装完了,然后就把这个命令丢进收藏夹吃灰。直到某天接口联调发现返回是空的、下载脚本卡在证书报错、自动化构建里那行 curl 悄悄失败了却没人发现,才反应过来这个看起来简单的命令其实有一整套行为逻辑没搞明白。curl 属于 Linux 常用命令里那种"用得最多、懂得最少"的典型代表,它既是下载工具,又是 HTTP 客户端,还是排错探针,三种身份混在一个二进制文件里。这篇内容就是把我这几年在运维、接口调试、CI 流程里踩过的 curl 相关的坑整理一遍,从最基础的方法和头部构造一路讲到证书报错、错误码定位、脚本化落地,适合刚接触 Linux 的朋友,也适合已经用了一两年但没系统梳理过的同学,看完至少能做到一件事:报错信息摆在面前,知道下一步该查哪里。
1. 从一条安装脚本拆起:curl 在真实环境里解决什么问题
很多人对 curl 的第一印象来自各种官方安装脚本,那条长长的命令看起来像咒语,其实每个字母都有明确含义。把这条命令拆开看,是理解 curl 设计哲学最快的入口,也是后面所有参数组合的基础。
1.1 安装脚本为什么普遍写成 curl -fsSL URL | sh
curl -fsSL https://example.com/install.sh | sh这行命令里塞了四个短选项,它们分别解决四个不同的问题。-f表示遇到 HTTP 错误状态码(4xx、5xx)时不要输出响应体,直接以非零退出码结束,这一条极其关键——如果没有-f,服务器返回一个 404 的 HTML 错误页,管道后面的 sh 会认认真真地尝试执行这段 HTML,结果就是一堆莫名其妙的语法错误,让人完全找不到方向。
-s是静默模式,关掉进度条和统计信息,避免这些内容混进管道污染脚本内容。-S是配合-s使用的,意思是"静默但不隐藏错误",当出错时仍然把错误信息打到标准错误。-L是跟随重定向,很多下载地址会先返回 301 或 302 跳到 CDN,没有-L就只能拿到一个空响应。
把这四个拼起来读就是:下载时安静点,但真出错了要告诉我,遇到跳转要跟过去,HTTP 状态不对就直接失败。这套组合几乎可以当作所有"下载后立即使用"场景的默认模板。我自己写脚本时,只要是拉配置、拉密钥、拉脚本,一律用-fsSL,很少例外。
注意:管道执行远程脚本这件事本身有风险,脚本内容在传输前你并没有看过。稳妥的做法是先
curl -fsSL url -o /tmp/install.sh,用less或head看一眼关键部分,确认无误再sh /tmp/install.sh。多花十秒钟,能避免很多尴尬。
1.2 curl 和 wget 的分工与取舍
新手常见的困惑是这两个工具到底该用哪个。简单说,wget 的设计目标是"递归下载整个网站",擅长镜像、断点续传、批量抓取;curl 的设计目标是"构造并发送一个 HTTP 请求",擅长自定义方法、请求头、请求体、认证方式。所以你会发现两个现象:写接口调试脚本的人几乎只用 curl,做爬取或备份静态资源的人偏爱 wget。
在容器镜像里这个区别更明显。很多精简基础镜像(比如 alpine)默认只带 curl 不带 wget,也有些镜像反过来。所以写 Dockerfile 的时候,别想当然地认为两个都在,要么显式安装,要么统一用同一个工具。我自己在 CI 脚本里统一用 curl,好处是参数语义一致,跨发行版行为差异小。
另外一个容易忽略的点是退出码语义。curl 的退出码是自己定义的一整套(比如 7 是连接失败、60 是证书校验失败),和 HTTP 状态码是两回事;wget 的退出码语义相对简单。脚本里做错误分支判断时,这个差异会直接影响你的逻辑写得对不对。
1.3 先确认手上的 curl 支持什么:版本与特性自检
同一个curl命令,在 CentOS 7 自带的 7.x 版本和现在常见的 8.x 版本上行为可能不一样,尤其是 HTTP/2、TLS 版本、--fail-with-body这类较新的选项。所以排错前先确认版本,这一步经常能直接锁定原因。
curl --version # 输出示例: # curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13 zlib/1.3 brotli/1.1.0 # Release-Date: 2023-12-06 # Protocols: dict file ftp ftps gopher http https ... # Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IPv6 ...这段输出信息量很大:第一行是版本号和底层库,libcurl后面跟着的 OpenSSL 版本决定了 TLS 能力,Protocols那一行列出了编译时启用的协议,Features那行列出了编译选项。如果Features里没有HTTP2,那么--http2参数是没意义的;如果没有SSL或者没有任何 TLS 后端名(OpenSSL、GnuTLS、Schannel 之类),那么所有 https 请求都会失败。
我遇到过一次很典型的情况:某台老服务器的 curl 报(35)握手失败,查了半天代码,最后发现是 curl 太旧,只支持到 TLS 1.0,而对方服务器早就关掉了 TLS 1.0。这种情况下除了升级 curl 没有别的办法。
2. 请求骨架三件套:地址、方法、头部
把 curl 当成一个"可以写在命令行的 Postman",这个类比基本成立。它做的事情就是:给定一个地址,选一个方法,附上一组头部和一个请求体,把数据发出去,再把响应带回来。理解这三样东西的构造方式,覆盖了日常八成以上的使用场景。
2.1 URL 里的那些写法:查询串、变量展开、多 URL 一次发
最基础的用法是直接把 URL 写在命令后面。如果 URL 里带查询参数,注意 shell 的转义问题:&在 shell 里是后台执行符号,所以必须加引号,写成curl 'https://example.com/api?page=1&size=20'。这一点新手极其容易错,忘了加引号的表现是命令只跑到第一个&就结束了,后面那一截被 shell 当成另一条命令,报出size=20: command not found这种莫名其妙的错误。
curl 支持一次传多个 URL,会按顺序依次请求:
curl -s -o /dev/null -w '%{http_code} %{url_effective}\n' \ https://example.com/a \ https://example.com/b \ https://example.com/c这个小技巧在批量探活时特别好用,一次就能拿到三个地址的状态码,不用写循环。URL 里还可以用[1-10]这种范围展开语法批量生成地址,比如curl 'https://example.com/img/[1-5].jpg' -O,会依次下载五张图片。范围可以是数字也可以是字母,还能带步长。
还有一个容易被忽略的细节是 URL 中的@、:、%等字符。如果路径里含有这些字符,最好用--url-query或者--data-urlencode来构造,而不是自己手工拼接字符串,手工拼接百分号编码出错率非常高,尤其是中文参数。
2.2 方法切换与重定向:-G、-L、--max-redirs
默认方法永远是 GET,需要切换时用-X POST、-X PUT、-X DELETE等。这里有个常识性坑:-X只是把请求行里的方法名换掉,并不会自动帮你加请求体或 Content-Type。所以curl -X POST https://api.example.com发出去的是一个没有 body 的 POST,很多框架会因为 body 缺失直接返回 400。
-G是一个反向操作,它的作用是把-d提供的数据拼成查询串放到 URL 后面,而不是放进请求体。也就是说curl -G -d 'a=1' -d 'b=2' https://example.com实际发出的是GET https://example.com?a=1&b=2。这个用法在需要传递一堆参数但接口只接受 GET 时非常省事。
重定向默认是不跟随的,需要-L。跟随重定向时有两个细节值得注意:一是跟随过程中如果跨域名,curl 默认不会把 Authorization 头带过去(这是安全设计),如果你的接口依赖这个头并且需要跨域跟随,得用--location-trusted,但要清楚它的风险;二是跟随次数默认上限是 30 次,防循环,可以用--max-redirs调整。
# 跟随重定向并显示最终地址和状态码 curl -sL -o /dev/null -w 'final: %{url_effective}\ncode: %{http_code}\n' https://example.com/redirect-me2.3 请求头与 Cookie:-H、-b、-c 的组合套路
-H添加请求头,可以重复使用多次。值得记住的一点是:curl 也有一套内置的默认头,比如User-Agent: curl/8.x、Accept: */*、Host自动从 URL 推导。用-H指定同名头会覆盖默认值,但如果想彻底删掉某个默认头,要用-H 'User-Agent:'这种"冒号后面留空"的写法。
Cookie 有两个参数:-b发送 Cookie,-c把响应里的 Set-Cookie 写入文件。这两个经常配合使用,模拟一次完整的登录加访问流程:
# 第一步:登录,把会话 Cookie 存到文件 curl -s -c /tmp/cookies.txt -d 'user=demo&pass=secret' https://example.com/login # 第二步:带着 Cookie 访问需要登录的页面 curl -s -b /tmp/cookies.txt https://example.com/dashboard这个模式在做接口自动化测试时特别常用,比在脚本里手工管理 Cookie 字符串省事得多。需要提醒的是 Cookie 文件里含有会话凭证,别随手提交到代码仓库,也别放在共享目录。
另外,-H里的内容会原样发送,所以千万别在头部里塞中文或换行符,那会直接破坏请求格式导致服务端解析错误。
3. 数据提交的四种形态:表单、JSON、文件、原始流
POST 请求的构造是 curl 使用中最容易出问题的一块,因为数据编码方式太多,选错了服务端就解析不到。这一节把常见的四种提交形态拆开讲清楚。
3.1 -d 与 --data-urlencode:中文和特殊字符的编码陷阱
-d是最常用的提交参数,它会做三件事:把方法改成 POST、把 Content-Type 默认设为application/x-www-form-urlencoded、把数据放进请求体。关键点在于,-d不会自动做 URL 编码。如果你的参数里有空格、&、=、中文,服务端拿到的就是脏数据。
正确做法是用--data-urlencode,它会自动对值做百分号编码:
# 错误:中文和空格会出问题 curl -d 'name=张三 李四' https://example.com/api # 正确:自动编码 curl --data-urlencode 'name=张三 李四' https://example.com/api # 也可以只对值编码,键自己写 curl --data-urlencode 'keyword@/tmp/query.txt' https://example.com/api@语法表示从文件读取内容再编码,这在提交一大段文本时很实用,避免了在命令行里拼接超长字符串。注意文件内容末尾的换行符也会被编码进去,如果服务端对格式敏感,需要先处理掉。
还有一个细节是多个-d之间会用&自动连接,所以不用自己加分隔符:
curl -d 'a=1' -d 'b=2' https://example.com/api # 实际请求体:a=1&b=23.2 JSON 提交与 Content-Type 的配对
现在大部分接口都是 JSON 格式,写法上必须显式指定 Content-Type,否则服务端可能按表单解析,然后报"参数缺失"这种让人一头雾水的错误:
curl -X POST https://api.example.com/users \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d '{"name":"demo","age":30}'这里有个非常好用但常被忽略的替代写法:把-d换成--json。这个选项会自动设置Content-Type: application/json和Accept: application/json,还能自动识别传进来的内容是 JSON 对象还是文件路径,少写两个头部,出错概率也降低。
curl --json '{"name":"demo"}' https://api.example.com/users curl --json @payload.json https://api.example.com/users如果 JSON 内容比较长,写成单行字符串容易出错(引号嵌套、转义),我自己的习惯是先在本地写一个payload.json文件,用--json @payload.json提交,调试通过后再考虑内联。这样还有个附带好处:请求体能直接被 jq 或者编辑器校验。
3.3 -F 上传文件与 multipart 边界
上传文件要用-F(form),它会把 Content-Type 设成multipart/form-data并自动生成 boundary。这和-d是两条完全不同的路径,混用会失败:
# 上传单个文件,字段名为 file curl -F 'file=@/path/to/report.pdf' https://example.com/upload # 同时带额外字段,并指定文件的 MIME 类型 curl -F 'file=@photo.jpg;type=image/jpeg' \ -F 'album=vacation' \ -F 'note=summer trip' \ https://example.com/upload # 上传多个同名字段(文件数组) curl -F 'files=@a.pdf' -F 'files=@b.pdf' https://example.com/upload@表示上传文件内容,<表示把文件内容当作字段值(相当于文本读取),这两个符号很容易记混。另外;type=;filename=这些修饰可以精确控制服务端看到的元信息,遇到上传后文件名乱码的问题时,往往就是 filename 编码没处理好。
提示:用
-F时不要再手动加-H 'Content-Type: multipart/form-data',因为 boundary 是 curl 自动生成的,手工指定的头缺了 boundary,服务端会直接解析失败。这是新手特别容易踩的一个坑。
3.4 从 Postman 导出 curl 之后要改哪几处
很多人是靠 Postman 里的"Code → cURL"导出命令的,导出来的东西能用,但直接贴到生产脚本里通常要做几处清理。第一处是--compressed,它本身没问题,但要求服务端正确返回压缩格式,某些老服务端配合某些 curl 版本会出现解析异常,调试时可以先去电。
第二处是 Windows 与 Linux 的引号差异。Postman 在 Windows 上导出的命令用双引号包裹,里面又有单引号,直接贴到 Linux 终端上会因为 shell 解析规则不同而报错,需要统一改成单引号包裹、内部双引号。
第三处是 Cookie 和 Authorization 头的处理。导出的命令经常把令牌硬编码在里面,直接放进脚本会有泄漏风险,建议改成从环境变量读取:
curl -X POST https://api.example.com/v1/orders \ -H "Authorization: Bearer ${API_TOKEN}" \ -H 'Content-Type: application/json' \ --json @order.json第四处是导出的 URL 经常带一堆无意义的查询参数(Postman 自己加的),上线前清掉,避免缓存键被污染导致 CDN 命中率下降。
4. 下载与落盘:-o、-O、-L、-f、断点续传与静默模式
curl 的另一大身份是下载器。下载相关的参数组合非常多,而且互相影响,理解它们的优先级能省下大量调试时间。
4.1 -o 与 -O 的差别,以及目录写权限的坑
-o是指定输出文件名,-O是使用 URL 里的文件名。看起来简单,但几个细节经常翻车。第一,-O保留的是 URL 最后一段,如果 URL 末尾带查询串,比如file.zip?token=abc,在某些版本里-O会把查询串也算进去,导致生成一个带问号的奇葩文件名。稳妥做法是显式用-o。
第二,如果目标目录不存在,curl 只会报错不会自动建目录。脚本里下载到/data/backup/这种路径时,得先mkdir -p。
第三,权限问题。以普通用户身份向/etc、/usr/local这类目录写文件会被拒绝,报错信息是Failed to open the file ... Permission denied,这个错误和网络无关,但经常被误判成下载失败。遇到这种报错先看路径和权限,不要急着查网络。
# 下载到指定文件,失败时快速退出 curl -fL --retry 3 -o /data/backup/app.tar.gz https://example.com/app.tar.gz # 静默下载,只输出最终状态码 curl -fsSL -o /tmp/config.yaml https://example.com/config.yaml \ && echo "下载成功" || echo "下载失败"4.2 -f 与 -s 与 -S 三兄弟:静默不等于隐藏错误
这三个选项组合是 curl 里最容易理解错的地方。-s 关掉进度显示,很干净,但同时也把错误信息吞掉了,脚本失败时你只能看到一个非零退出码,完全不知道发生了什么。-S 就是把错误信息找回来,且仅在出错时输出。
实际写脚本时我基本固定用-fsS:静默、失败即退出、出错时打印原因。这三个字母的组合比单独用-s或者不用参数都要好。至于-f,需要特别注意它的一个副作用:它会让 4xx、5xx 响应体不再输出到 stdout,也就是你看不到服务端返回的错误详情了。如果调试阶段需要看错误详情,用--fail-with-body,它保留了-f的退出码语义,同时把响应体打出来。
# 既想要非零退出码,又想看错误响应体 curl -sS --fail-with-body https://api.example.com/maybe-404 echo "退出码:$?"curl 的退出码是独立于 HTTP 状态码的一套编码。--fail系列会让 4xx/5xx 映射成退出码 22,连接层错误则是 7、28、35、60 这些。脚本里可以根据退出码做不同处理,比如 22 说明服务端逻辑拒绝,7 说明网络或端口不通,两者的排查方向完全不同。
4.3 大文件下载:-C -、--limit-rate、超时参数
下载几百 MB 或几个 GB 的文件时,断点和限速就很重要了。-C -表示自动从已下载的字节位置续传,配合-o使用:
curl -fL -C - -o /data/big.iso https://mirrors.example.com/big.iso这个参数要求服务端支持 Range 请求,如果不支持,curl 会从头开始覆盖下载,不会有任何提示,所以用之前最好确认一下。限速用--limit-rate,单位可以是 k、m、g,比如--limit-rate 2m表示限制在每秒 2MB,适合在带宽紧张的线上机器上做后台下载时避免打满带宽。
超时方面有三个参数需要区分清楚:--connect-timeout限制建连阶段,--max-time限制整个请求从开始到结束的总时长,--speed-time配--speed-limit是在"速度低于阈值持续多少秒"时断开。生产环境里这三个建议都设上,尤其--max-time,否则一个卡死的连接可能让你的脚本挂到天荒地老,在 CI 里表现为任务超时被强制终止,日志里什么都看不到。
curl -fL --connect-timeout 10 --max-time 600 \ --retry 3 --retry-delay 5 \ -C - -o /data/big.iso https://mirrors.example.com/big.iso配合--retry时要注意,默认只对"瞬时错误"重试,比如连接超时、DNS 解析失败,对 HTTP 4xx 是不会重试的。如果希望 5xx 也重试,加--retry-all-errors,但要注意别对幂等性有问题的接口乱用,可能导致重复下单这类副作用。
5. 证书与错误码:curl: (60)、(35)、(7) 的排查链路
这一节应该是全文最实用的部分。curl 的报错信息其实写得很清楚,只是格式看起来吓人。下面把几个高频错误逐个拆开,给出完整的排查顺序,而不是直接丢一个"加上 -k 就好了"的答案——-k关掉证书校验,在测试环境偶尔用用还行,放到生产脚本里等于把 HTTPS 的安全价值全部扔掉。
5.1 curl: (60) SSL certificate problem 的三种成因
完整报错通常是这样的:
curl: (60) SSL certificate problem: unable to get local issuer certificate More details here: https://curl.se/docs/sslcerts.html这个错误的核心含义是:客户端拿到的服务端证书,无法用本地的 CA 信任库验证出一条完整链路。成因基本落在三种情况上。
第一种是系统 CA 证书包缺失或过期。精简的容器镜像为了减小体积,经常不装ca-certificates,或者装的是很早以前的版本,而现在的证书链普遍用 Let's Encrypt 之类的新根证书,老库当然验不过。解决办法是安装或更新 CA 包,Debian 系用apt-get install -y ca-certificates,RHEL 系用yum install -y ca-certificates,装完记得update-ca-certificates。
第二种是服务端证书链不完整。很多中间证书没配好,浏览器会自动补全,所以浏览器访问正常,但 curl 严格校验就失败。验证方法是:
# 查看服务端返回的完整证书链 openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null # 只看证书主题和签发者 echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates如果输出的链条只有一张叶子证书、没有中间证书,那问题在服务端,需要运维把完整链配上去。这种情况你在客户端怎么折腾都没用。
第三种是目标用的是自建 CA 或内网证书。这时候正确的做法不是-k,而是把自建 CA 的根证书导入本地信任库,或者用--cacert指定:
curl --cacert /etc/pki/ca-trust/source/anchors/internal-ca.crt https://internal.example.com把自建 CA 加到系统信任库的好处是,所有工具(curl、wget、Python requests 只要用的是系统库)都能受益,而不用每次都带参数。
5.2 curl: (35) 握手阶段的失败信息怎么读
(35)是 TLS 握手失败,这个错误码覆盖的范围比 60 宽得多,具体原因要看冒号后面的描述。常见的几种描述和对应原因:
| 报错尾部信息 | 常见原因 | 排查方向 |
|---|---|---|
tcp connection reset by peer | 握手阶段被对端或中间设备重置 | 检查是否被防火墙/负载均衡拒绝、TLS 版本协商是否失败 |
next InitializeSecurityContext failed | 系统 TLS 后端握手失败 | 常见于证书吊销检查失败、系统时间不对 |
error:14094410:SSL routines | 协议或加密套件不匹配 | 检查服务端是否已禁用老版本 TLS |
wrong version number | 明文 HTTP 端口被当成 HTTPS 访问 | 检查端口和协议是否写错 |
这里有两个经验点值得单独说。一个是系统时间。证书有效期校验依赖本机时钟,如果服务器时间漂移了几年,所有证书都会"未生效"或"已过期",报的却是各式各样的握手错误,特别难猜。遇到莫名其妙的 TLS 报错,先date看一眼时间。另一个是吊销检查。Windows 上有时候会看到CRYPT_E_REVOCATION_OFFLINE这类信息,说明吊销列表拉不下来,可以临时用--ssl-no-revoke绕过(只建议在明确知道自己在做什么的情况下使用)。
排查握手问题时,-v是最直接的工具,它会把 TLS 版本、加密套件、证书信息全打出来:
curl -v https://example.com 2>&1 | grep -Ei 'SSL|TLS|certificate|ALPN'5.3 curl: (7) Failed to connect 的逐层定位
curl: (7) Failed to connect to example.com port 443: Connection refused(7)是连接层错误,说明 TCP 三次握手没完成,问题在 curl 发出请求之前就发生了。排查要分层走,顺序不要乱。
第一层是 DNS。先确认域名解析出来的 IP 是不是你预期的那个:
getent hosts example.com dig +short example.com如果解析出来是内网地址或者解析失败,那后面都不用查了。有些环境里/etc/resolv.conf被改乱,表现就是所有域名都解析不了,但 IP 直连正常。
第二层是网络可达性。用ping或者nc -zv example.com 443确认端口是否可连。注意有些服务器禁 ICMP,ping 不通不代表端口不通,别被误导。
第三层是监听状态。如果目标是你自己的服务,用ss -lntp | grep 443看端口有没有真的在监听。我遇到过好几次"服务启动了但绑到了 127.0.0.1",从外部访问自然连不上,日志里却看不出任何异常。
第四层是防火墙和安全组。本地 iptables、云平台安全组、中间的网络策略,任何一层拒绝都会表现为同样的Connection refused或超时。区分方法很简单:refused通常意味着包到了但对端没监听或被明确拒绝,长时间卡住然后超时更可能是被静默丢弃。
5.4 错误码速查表与 --fail-with-body
把常用退出码整理成一张表,脚本里做分支判断时可以直接对照:
| 退出码 | 含义 | 典型场景 |
|---|---|---|
| 0 | 成功 | 正常完成 |
| 1 | 不支持的协议 | URL 里的协议名写错 |
| 3 | URL 格式错误 | 少了协议头或拼写错误 |
| 6 | 无法解析主机 | DNS 问题或域名写错 |
| 7 | 无法连接 | 端口不通、服务未启动、防火墙拦截 |
| 22 | HTTP 返回错误码 | 配合 -f / --fail 使用,4xx、5xx 会映射到这里 |
| 28 | 操作超时 | 网络慢、服务端处理慢、超时参数设太短 |
| 35 | TLS 握手失败 | 协议版本、证书链、加密套件问题 |
| 47 | 重定向次数过多 | 重定向循环 |
| 52 | 服务端无响应 | 连接建立了但没返回数据 |
| 56 | 接收数据失败 | 传输中断、连接被重置 |
| 60 | 证书校验失败 | CA 库缺失、证书链不全、自签名证书 |
脚本里做判断的推荐写法:
if ! curl -fsSL --max-time 30 -o /tmp/out.json https://api.example.com/data; then code=$? case $code in 7) echo "网络不可达,检查服务与防火墙" >&2 ;; 22) echo "服务端返回错误状态码" >&2 ;; 28) echo "请求超时" >&2 ;; 60) echo "证书校验失败,检查 CA 库或证书链" >&2 ;; *) echo "curl 失败,退出码:$code" >&2 ;; esac exit 1 fi这套分支看起来啰嗦,但它能在凌晨三点被报警叫醒的时候,让你一眼看出是网络问题还是业务问题,省下的时间远比写这几行的成本高。
6. 把 curl 当调试工具用:-v、--trace、-w 与耗时拆解
curl 不只是发请求的工具,它本身就是一个相当强的网络调试器。会用这几个输出选项之后,很多"到底卡在哪一步"的问题不用抓包就能定位。
6.1 -v 与 -i 输出该看哪几行
-v把整个过程分成三部分输出,每行前面带符号标识:*开头的是连接和协议层信息,>开头的是 curl 发出的内容,<开头的是收到的内容。用-v时先看*那部分,里面包含 DNS 解析结果、尝试连接的 IP 和端口、TLS 版本和加密套件、最终使用的 HTTP 版本,这几条信息能快速排除掉一大批猜测。
curl -v https://example.com/api 2>&1 | head -40-i只输出响应头加响应体,适合快速看状态码、响应头里的缓存策略、重定向位置、Set-Cookie 内容。相比-v,-i的输出更干净,没有连接层噪音,日常调试接口我更常用-i。
curl -si https://example.com/api | head -20需要注意的是,-v会把 Authorization、Cookie 这类敏感头原样打印出来,所以在 CI 日志里使用要谨慎,可能造成凭证泄漏。这也是为什么我建议在脚本里用-sS而不是-v。
6.2 -w 自定义输出格式:一次拿到状态码与各阶段耗时
-w是 curl 里被低估最多的选项。它能按模板输出一堆内置变量,把一次请求的各阶段耗时、状态码、实际地址、传输字节数全部打印出来,配合-o /dev/null就得到一个轻量的接口性能探针。
curl -s -o /dev/null -w ' dns解析: %{time_namelookup}s 建连: %{time_connect}s TLS握手: %{time_appconnect}s 首字节: %{time_starttransfer}s 总耗时: %{time_total}s HTTP状态: %{http_code} 下载字节: %{size_download} 实际地址: %{url_effective} 重定向次数: %{num_redirects} ' https://example.com/api这几个时间点的关系能说明很多问题。time_namelookup大说明 DNS 慢,通常是 DNS 服务器配置有问题或者解析链路太长。time_connect与time_namelookup的差值大说明网络往返延迟高。time_appconnect与time_connect的差值就是 TLS 握手耗时,如果这个值异常大,通常是证书链太长或者吊销检查在超时。time_starttransfer与time_appconnect的差值接近服务端处理时间,能直接反映出后端的响应速度。这几个数字一摆出来,问题归属到哪一层就非常清楚了。
提示:
-w的模板字符串里换行可以直接写,但如果放在 shell 的双引号里,%不需要转义。想输出字面的百分号,写%%。
6.3 抓 SSE 流式接口与长时间连接的注意事项
现在越来越多接口是流式返回的(服务端持续推送数据块),用 curl 调试这类接口时,默认的缓冲行为会让你感觉"卡住了没有任何输出"。实际上数据在陆续到达,只是被行缓冲挡住了。
# 关闭缓冲,实时看到流式输出 curl -N -s https://example.com/stream/events # 加上超时限制,避免流式连接一直挂着 curl -N -s --max-time 60 https://example.com/stream/events-N是关闭输出缓冲的关键参数。另外流式接口通常不会正常"结束",连接会一直保持,所以--max-time几乎是必须的。如果只是想观察几十秒的数据格式,用timeout 30 curl -N -s ...从外部加时限也可以,两种方式效果类似。
调试流式接口时还有个实用技巧:把输出重定向到文件再逐行看,避免终端被刷屏。
timeout 20 curl -N -s https://example.com/stream/events > /tmp/stream.log wc -l /tmp/stream.log tail -5 /tmp/stream.log这样能清楚看到这段时间内到达了多少条事件、格式对不对、有没有心跳包,比盯着滚动的终端有用得多。
7. 脚本化落地:把 curl 放进运维与 CI 流程
命令行敲和写进脚本是两回事。脚本里要考虑退出码传播、重试策略、凭证管理、并发控制,这些才是 curl 在生产环境里真正的难点。
7.1 退出码与 set -e 的配合
shell 脚本里如果写了set -e,任何一条命令返回非零都会让脚本立即退出。curl 只要没加--fail,HTTP 500 也会返回退出码 0,脚本会当成功继续往下跑,这是很多"构建成功了但产物是错的"问题的根源。
所以脚本里使用 curl 有两条铁律:下载和调用接口一律加-f或--fail-with-body;如果有哪次失败是可以容忍的,显式用|| true或者在if条件里调用,不要依赖隐式行为。
#!/usr/bin/env bash set -euo pipefail API=https://api.example.com # 失败的调用会让脚本退出,且能看到错误详情 curl -fsS --fail-with-body --max-time 20 "${API}/health" -o /tmp/health.json # 允许失败的调用,明确写在 if 里 if ! curl -fsS --max-time 10 "${API}/optional" -o /tmp/optional.json; then echo "可选接口不可用,继续执行" >&2 fiset -o pipefail这一条在管道场景里也很重要。curl ... | sh这种写法如果没有 pipefail,sh 的退出码会覆盖 curl 的退出码,下载失败但脚本解析失败被掩盖的情况就会发生。加上 pipefail,管道里任何一段失败都会传播出来。
7.2 重试、超时与并发控制的参数组合
生产脚本里的 curl 调用基本都应该带上超时和重试。推荐的组合是:
curl -fsSL \ --connect-timeout 10 \ --max-time 60 \ --retry 3 \ --retry-delay 2 \ --retry-max-time 180 \ --retry-connrefused \ -o /tmp/artifact.tar.gz \ https://example.com/artifact.tar.gz这套参数的含义是:建连最多等 10 秒,整个请求最多 60 秒,失败后重试 3 次、每次间隔 2 秒、重试总时长不超过 180 秒,遇到连接被拒绝也重试。--retry-connrefused这个选项在服务刚启动还没就绪的场景下很有用,比如容器编排里等待依赖服务起监听。
需要批量请求时,不要写一个 for 循环串行跑,几十个请求会慢到不可接受。用xargs -P做并发控制:
cat urls.txt | xargs -P 8 -I {} sh -c ' code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "{}") echo "{} $code" '-P 8表示最多 8 个并发。这个数字别开太大,容易把对端打挂,也会让本机的文件描述符和连接数紧张。8 到 16 之间对大多数场景是合适的。
7.3 认证信息不进历史记录:--netrc、-K 配置文件
直接在命令行里写-H 'Authorization: Bearer xxx'有两个问题:一是会进 shell 历史记录,二是会出现在ps输出里,同机器上的其他用户能看到。生产脚本里应该从环境变量读取,或者用 curl 的配置文件。
-K可以指定一个配置文件,把常用参数写进去:
# /etc/curlrc-app (权限设为 600) header = "Authorization: Bearer abc123" connect-timeout = 10 max-time = 60 retry = 3 silent = true show-error = truecurl -K /etc/curlrc-app https://api.example.com/data配置文件的语法是每行一个"选项名 = 值",和命令行参数一一对应,参数名前面不用加--,短横线保持一致。这样做的好处是凭证不出现在命令行和历史里,同时把一批公共参数集中管理,脚本里只保留和业务相关的部分,可读性提升明显。
另一个方案是--netrc-file,专注于用户名密码认证,格式是三段式:
machine api.example.com login myuser password mypassword配合curl -n --netrc-file /path/to/netrc使用。这个文件同样要设置成chmod 600,并且不要提交到任何代码仓库。如果是 CI 环境,把内容放进受保护的密钥变量,在流水线里临时生成这个文件、用完立即删除,是比较稳妥的做法。
注意:配置文件里的选项是"默认值",命令行里显式写的参数会覆盖它。所以不用怕全局配置会影响所有调用,需要临时覆盖的时候直接加参数就行。
用 curl 这些年,我最大的体会是这个工具的报错其实非常诚实,它在尽力告诉你哪一层出了问题,只是信息被压缩成了几个数字和短语,不熟悉的人容易慌。真正值钱的经验不是记住所有参数,而是养成一个习惯:看到报错先确认版本和系统时间,再按 DNS、网络、TLS、HTTP 这个顺序分层排查,一层确认没问题再往下一层走,别跳步。另外强烈建议在自己的机器上准备一份~/.curlrc,把connect-timeout、max-time、retry、silent、show-error这几项设成默认值,你会发现日常调试的体验顺滑很多,至少在终端里不会再被卡死的连接和一堆进度条打扰。