资讯动态

curl SSL报错与调试实战:unexpected EOF、Git 56、jq

发布时间:2026/9/26 5:12:10 来源:尧图企业网站定制
如果你在终端里敲过curl大概率被这两种东西折磨过一种是输出结果堆成一坨的JSON另一种是突然冒出来的SSL报错。我最早用curl只是为了下载文件直到开始调接口、排查网络问题才发现自己严重低估了这个命令。curl: (35) error:0a000126:ssl routines::unexpected eof while reading、error: rpc failed; curl 56 schannel: server closed abruptly这类报错我在不同环境里至少帮人排过二十次每次都能带出一个新的“没想到”。这篇不是把curl手册抄一遍而是从实际项目里最常见的错误和用法出发把排查思路、终端调试技巧、以及在Windows 7这种老环境里怎么凑齐curl和ssh的方案一起串起来。不管你是刚接触命令行的新手还是被Git拉取大仓库折磨过的老手下面这些内容应该都能直接用在你的日常环境里。1. 从一次“unexpected EOF”说起curl最阴间的SSL报错排查1.1 报错里的每个字段都在说什么curl: (35) error:0a000126:ssl routines::unexpected eof while reading这条报错可以拆成三段看(35)是curl的退出码对应“SSL连接错误”0a000126是OpenSSL库的错误码unexpected eof while reading是字面意思curl在读取TLS数据时连接被对端提前关闭了。这个“提前关闭”在实战中基本有四种情况。第一种服务器的TLS协议版本太老只支持TLS 1.0/1.1而新版curl默认最低协商TLS 1.2握手阶段双方谈不拢连接被直接断开。第二种服务器配置了证书链但只发送了叶子证书没把中间证书发给客户端curl校验链失败后中止握手。第三种中间的网络设备防火墙、负载均衡器、安全网关在握手过程中注入干扰或直接RST。第四种服务器本身压力大、超时应用层主动断了连接。如果只是偶发一次大概率是第四种如果每次必现优先查协议版本和证书链。为了方便定位我把curl常见的几个退出码整理成了一张表排查时直接对着看curl退出码含义常见场景7连接失败端口不通、防火墙DROP、服务器未启动35SSL连接错误TLS握手失败、协议版本不匹配、SSL库错误56接收数据失败服务端连接异常关闭、发送RST、代理断开60证书验证失败自签证书、证书链不完整、证书过期28操作超时连接建立超时、传输超时1.2 用openssl s_client反向验证到底是谁先断的排查SSL类问题我建议你先别在curl本身上死磕直接跨到openssl s_client去看握手过程。这条命令能完整模拟一次TLS握手并且把服务器返回的证书链、加密套件、协议版本都打印出来openssl s_client -connect example.com:443 -tls1_2 -showcerts如果这条命令能顺利走完并打印出Verify return code: 0 (ok)说明TLS层没问题问题大概率出在HTTP层或中间设备上。如果openssl同样报unexpected eof while reading那就基本确认是服务器或网络链路的问题跟curl的配置无关。另一个非常常见的坑是服务器只送了叶子证书。用-showcerts看到的证书列表只有一条没有中间的签发证书客户端在构建信任链时无法递到根证书。这种情况openssl s_client会显示verify error:num20:unable to get local issuer certificatecurl这边往往报的是SSL certificate problem而不是eof。解决办法不是“忽略证书”而是让运维把中间证书补全到服务器配置里或者你在本地手动安装中间证书。如果公司环境允许抓包用Wireshark过滤tcp.port 443看最后几个包的结束方式。FIN包代表对端正常关闭了TCP连接但没发TLS层的close_notify这多半是超时或配置问题RST包代表对端直接重置基本可以确定是服务端应用崩溃或主动踢掉连接。知道了区别跟服务端沟通时就能说出个所以然而不是笼统来一句“curl报错了”。1.3 强制TLS版本与忽略证书两个“能跑但别滥用”的应急方案当你确认是协议版本不匹配时可以先用强制参数验证一下猜想curl --tlsv1.0 https://old-system.example.com curl --tlsv1.1 https://old-system.example.com curl --tlsv1.2 https://old-system.example.com哪条能通就说明服务器最高支持到哪个版本。对于测试环境临时用--tlsv1.0续命是可行的但生产环境还是建议推动升级服务端TLS。TLS 1.0/1.1早已被主流标准废弃留着它不只是curl的问题还有真实的安全风险。至于-k或--insecure参数我的态度很明确只能在自签名证书的测试环境用而且要写清楚为什么用。有些同事喜欢在脚本里一上来就-k结果生产环境的证书过期了都没发现直到服务挂了才追查。你要是实在需要忽略证书至少把范围限定在单条命令里别做成全局别名。能用--cacert指定根证书就比-k稳妥得多下载对方提供的CA证书文件然后curl --cacert ca.pem https://private.example.com这样既绕过了系统信任库的问题又保留了证书校验能力。2. 参数只会-C把curl当HTTP客户端用还差这几个细节2.1 你不需要每次都写 -X POST很多初学者调试POST接口时习惯写成curl -X POST http://api.example.com/login -H Content-Type: application/json -d {user:admin}其实在提供了-d或--data的情况下curl会自动把请求方法转为POST额外写-X POST并不是必须的。-X真正派上用场的地方是自定义方法例如curl -X PUT、curl -X DELETE或者你想发一个“带请求体的GET请求”这种非典型用法。这里有个小坑我踩过一次当-d后的字符串以开头时curl会把它当成文件路径来读取。比如你要发送{timestamp:2025-01-01}这种内容直接-d就会报“Couldnt open file”。解决方法是改用--data-raw它不会解析开头的数据原样发送。类似的还有--data-urlencode它会对内容做URL编码适合传查询参数。选择哪个取决于你要发原始数据还是编码后的数据搞混了容易在接口里收到一堆意外字符串。2.2 调试接口的黄金组合-v、--trace和-w-v会打印HTTP请求行、请求头、响应头以及TLS握手信息日常调试绝对够用。但如果你需要看SSL层更细的内容比如协商出来的加密套件、证书详情-v就不够了。这时用--trace或--trace-ascii它会把整个收发过程以十六进制加ASCII的形式输出行数很多建议配合-o /dev/null来用或者存到文件再慢慢翻。-w是我最想推荐的重型参数它能在请求结束后输出你指定的统计信息比如curl -s -o /dev/null -w 耗时: %{time_total}s\nDNS: %{time_namelookup}s\n连接: %{time_connect}s\nTLS: %{time_appconnect}s\n首字节: %{time_starttransfer}s\n下载: %{size_download} bytes\n https://example.comtime_appconnect和time_starttransfer的差值就是服务端从收到请求到返回第一个字节的应用处理时间。如果这个值很大说明服务端业务逻辑慢如果time_appconnect本身很大说明TLS握手环节有瓶颈比如证书链过长、服务器CPU处理密钥交换吃力或网络往返延迟太高。这段命令配合shell循环能轻松做出一份简易的接口拨测报告比引入整套监控工具轻得多。2.3 跟随重定向和Cookie管理的注意点-L跟随重定向看起来省事但有个安全细节很多人没注意-L默认在重定向时不会携带Authorization头这是保护你的但如果你需要带得用--location-trusted同时还意味着原始请求里可能有敏感信息会被转发到目标地址。我的习惯是先手动看Location头确认安全了再用-L。Cookie操作分两个参数-b是向服务器发送Cookie-c是把响应里的Set-Cookie保存到文件。登录接口的典型流程是curl -c login.txt -d useradminpass123456 http://api.example.com/login curl -b login.txt http://api.example.com/profile第一条命令把登录后的Cookie写进login.txt第二条带上这个Cookie访问需要认证的接口。如果你用的是JSON接口返回的token往往在响应体里用jq提取后存成变量继续传给下一个请求效果更直接。Cookie文件这种方式更多是模拟浏览器会话用的两条命令之间不需要手动粘贴内容很适合脚本化。2.4 超时参数没有超时的curl是危险品给curl加超时是每个脚本应有的基本素养。--connect-timeout控制连接建立的最大等待秒数--max-time控制整个请求的最大秒数。比如curl --connect-timeout 5 --max-time 30 http://api.example.com如果不加一旦服务器路由黑洞或服务挂起你的脚本就卡在原地CI任务可能就这么耗上一小时。我在写监控脚本时甚至会加--retry 3 --retry-delay 2但要注意--retry默认只在某些错误码时生效并不是所有失败都重试。超时和重试组合得好能让脚本在弱网环境里稳定很多。3. Git报错curl 56Windows上“server closed abruptly”的排查实录3.1 为什么你看到的不是“Connection reset”而是“missing close_notify”Git for Windows用户应该都遇到过或搜到过这个报错error: rpc failed; curl 56 schannel: server closed abruptly (missing close_notify)先拆字段curl 56代表数据接收失败schannel是Windows自带的TLS安全通道也是Git for Windows默认的SSL后端missing close_notify是这个问题的关键——TLS协议里通讯双方在结束会话前会互发一个close_notify警报告诉对方“我要正常关闭了”。现在的情况是对方在没发这个警报的情况下直接把连接断掉了。这是Schannel自己特有的报错描述。Linux上使用OpenSSL后端时同样的提前断开往往被描述为CONNECTED、SSL_read: Success或unexpected eof措辞不一样但本质都是“对方没按套路关连接”。触发场景非常集中clone大仓库、fetch大量对象、从某些服务器持续拉取数十MB数据时。服务器一般不会明着拒绝你而是在传输过程中因为超时、内存压力或代理设置直接掐断TCP连接Git客户端就收到了这个不完整的TLS会话。3.2 从缓冲区下手http.postBuffer只是第一步针对Git大仓库出错网上最流行的办法是调大HTTP缓冲git config --global http.postBuffer 524288000这个值代表Git在和远端通信时使用的HTTP缓冲区大小默认只有1MB。早期部分Git版本在收到大量数据时如果缓冲区太小会把int溢出成负数导致传输失败。调大到500MB确实能解决一部分老版本问题。但这几年新版的Git早就不太依赖这个参数了所以如果调了没用别奇怪继续往下排查。第二步关闭HTTP压缩git config --global http.compression false有些服务端在启用压缩传输时和Schannel的组合存在兼容性问题导致连接在解压过程中突然断开。关闭压缩会牺牲一些带宽但换来的是稳定。对于内网连接通常无所谓对于跨公网拉取大仓库你可以先试一次能通再加回true比较传输速度。第三步调整HTTP版本。Git默认使用HTTP/1.1有些服务器和Schannel在处理HTTP/2的流控制时更脆弱如果你的Git版本支持可以试试git config --global http.version HTTP/1.1强制走HTTP/1.1后少了一层HTTP/2的帧多路复用排错也更简单。很多诡异断开问题就是这么好的。3.3 换TLS后端从Schannel切回OpenSSL如果以上配置都改了还是不行剩下的大招是把Git的SSL后端从Schannel换成OpenSSL。Git for Windows在安装时有一个“Select Components”步骤里面可以选择“OpenSSL”作为SSL后端。已经装完的可以下载一个非安装版的PortableGit里面自带OpenSSL然后配置PATH使用它。不过要注意运行时切后端没有统一的git config开关官方文档也不建议在不同版本间混用。最可控的方式是先用git --version --build-options查看当前Git使用的SSL后端如果是schannel再下载一个明确标注“OpenSSL”的构建版本。换完之后同样的git clone命令可能就不再报curl 56了因为OpenSSL对缺失close_notify的容忍度更高更接近Linux上的行为。3.4 直接绕开走SSH协议克隆说实话爬过大仓库的坑之后我对HTTP协议拉取大仓库已经有点ptsd了。现在我的首选方案是能用SSH就用SSH。在代码托管平台配置好公钥之后把仓库地址从https://github.com/user/repo.git换成gitgithub.com:user/repo.git客户端走的是SSH协议根本不经过libcurl自然不存在curl 56这种问题。SSH不仅绕开了Schannel在推送大对象时也更稳定。我实测过同一个2GB仓库HTTP协议拉取三次两次触发server closed abruptly切换SSH后一次通过。当然SSH需要开放22端口有些网络环境会封锁那就只能回去和HTTP死磕。但至少你的排查路径里应该包含这一步别只盯着postBuffer。顺带提一句如果公司使用自建Git服务器且遇到这个报错也可以检查服务器端的HTTP超时设置。比如Nginx的proxy_read_timeout设太小就会在长耗时请求时主动断开客户端看到的就是连接被关闭。运维侧把超时调到120秒以上往往比客户端折腾半天配置更有效。4. 终端接口调试流水线curl的输出接上jq才算完整4.1 为什么我不用python -m json.tool而推荐jqcurl返回的JSON在没有管道处理时长得跟灾难现场一样就算不嵌套一长串内容也足够让人眼晕。很多教程会让你用python -m json.tool格式化但它做不了筛选你仍然得对着大段输出找想要的字段。jq不一样它是专门为JSON设计的命令行工具格式化只是最基础的功能真正的杀手锏是查询和过滤。以这个接口为例curl -s https://api.example.com/items | jq .[] | select(.price 100) | {name, price}一条命令就把数组里价格大于100的元素的name和price提取出来输出成新的JSON对象。这在调试分页、筛选异常数据时特别爽。配合条件判断还能实现简单的断言比如检查返回码curl -s https://api.example.com/health | jq -e .status ok拿脚本的判断逻辑去验证接口可用性比正则匹配响应文本靠谱得多。4.2 别把-w的内容混进JSON耗时写到标准错误我见过不少人把-w和jq一起用写出了这种命令curl -s -w \n耗时: %{time_total}s\n https://api.example.com | jq .结果jq直接报错因为耗时文本混进了JSON流。正确的姿势是让curl把响应体存到文件同时把耗时打到标准错误这样标准输出仍然保持纯净curl -s -w time_total:%{time_total}\n -o /tmp/resp.json https://api.example.com 12; jq . /tmp/resp.json从执行效果看终端会先打印耗时在标准错误里然后jq再把格式化后的JSON打出来。两条信息都保留了互不干扰。这个方法我第一次用就觉得相见恨晚之后所有的curl拨测都改成了这种样式。如果你要的是“响应体耗时全给jq处理”那就在jq侧操作变量别污染管道。4.3 组合成自己的调试脚本我不想每次调接口都敲一大串Header所以在~/.zshrc里放了一个函数api() { local url$1 shift curl -s -H Authorization: Bearer $TOKEN -H Content-Type: application/json $ $url | jq . }TOKEN从环境变量里读取不落在命令行历史里。之后调试api https://api.example.com/users -X POST -d {name:test}参数透传给curl输出自动jq格式化。如果你还想要-i保留响应头就在curl命令里加-i然后jq前面先用sed把header段剥离。不过这种做法有点脏我更习惯另开一个终端curl -i看原始头。iTerm2用户也能在Preference里设置触发器让带http://的文本一键通过curl请求但那些都是锦上添花的事核心还是把curl和jq的管道结构掌握了到哪个终端都一样。5. Windows 7或许是最“难搞”的curl环境顺带把ssh补齐5.1 别用一键安装包直接把curl.exe放进System32Windows 7没有自带curl也没有OpenSSH想在cmd里用ssh-keygen更是不可能。以前网上好多“一键安装包”装完一堆捆绑软件为了跑一条curl命令实在不划算。最干净的做法是去curl官网下载Win32/Win64的zip包解压后把curl.exe以及配套的curl-ca-bundle.crt直接复制到C:\Windows\System32下然后开一个新cmd窗口验证curl --version这里有两个细节要注意第一新版curl对Windows 7的支持正在逐步削减部分构建在Win7上会提示缺少VCRUNTIME140.dll你需要装Visual C 2015-2022运行库。如果装了运行库还报错就下稍微旧一点的版本比如7.78.0左右的构建反而更稳。第二证书文件必须被curl找到。新版本的curl会默认查找同目录下的curl-ca-bundle.crt所以不要只copy exe而忘了bundle。没有这个文件HTTPS请求会直接报证书错误。命令行测试通过后再顺手把zip包里的jq.exe也丢进System32。这样Win7的老终端就同时拥有了HTTP请求、TLS支持、JSON格式化三个能力日常调试完全够用。5.2 SSH怎么办Git for Windows自带的是个宝Windows 7上装OpenSSH的正规路线是安装微软的补丁包要求64位系统、补丁齐全限制很多。我更推荐直接装Git for Windows它自带的C:\Program Files\Git\usr\bin目录里包含了ssh.exe、ssh-keygen.exe、ssh-copy-id等一整套工具。你并不需要为了ssh去单独折腾系统级OpenSSH把这个目录加入PATH即可。setx PATH %PATH%;C:\Program Files\Git\usr\bin重新打开cmd敲ssh -V能看到输出就说明可用了。这个方法的好处是Git for Windows在提交、拉取时本身也是用这同一套ssh密钥路径、配置都统一不需要额外维护两套环境。缺点是这个ssh依赖Git安装目录里的MinGW运行库如果同时装了两个版本Git可能出现PATH顺序把老版本带进坑里的情况。解决办法就是检查where ssh确保指向的是你想要的目录。5.3 老机器终端的最终验收环境配完了用一条命令自检curl --version ssh -V jq --version三条命令都能输出版本信息说明基本可用。然后跑一个稍微完整的例子curl -s https://api.github.com/repos/curl/curl | jq .stargazers_count如果你能看到一个数字恭喜你这台Windows 7的终端能力已经追平大多数现代Linux桌面了。我在一台老ThinkPad上这么配完把它当成内网脚本执行器用来跑定时任务稳了两年多。很多人觉得Windows 7不值得折腾其实关键不是系统新不新而是你能否在它上面构建出一套可用的命令行工具链。补上curl、ssh、jq这三块拼图老机器照样能发光发热。最后再分享一个小经验不管Windows还是macOS我一直建议把curl的输出默认走一遍jq、把超时参数写进脚本、把SSH作为大仓库传输的首选。这几个习惯帮你避开了我当年踩过的绝大多数坑。尤其是--max-time哪怕只是加个10秒、20秒也能让你的脚本在异常环境里显得从容很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑