1. 用之前先搞清参数跑到哪里去了:HTTP请求的结构与curl的映射
1.1 一个URL里到底能装下几段参数
我在帮别人排查接口问题的时候,发现很多人对"参数放哪里"这件事本身就很模糊。他们知道要往URL里加参数,但不知道为什么有时候参数要放在?后面,有时候又要放在请求体里,更不清楚这两者对后端服务来说到底有什么区别。
先看一个最基础的HTTP请求在网络上长什么样。假设我们请求了这个地址:
https://example.com/api/user/query?name=zhangsan&age=25这个URL可以被拆成几段:
https:协议,告诉curl用什么方式加密传输。example.com:主机名,对应服务器的IP或域名。/api/user/query:路径,对应后端某个接口路由。?name=zhangsan&age=25:查询字符串,这是GET请求带参数的核心载体。
?后面跟的name=zhangsan是第一个参数,&用来连接第二个参数age=25。后端在接收时,会根据框架不同自动把这些键值对解析成一个字典或对象。
很多新手容易犯的第一个错误是:在写curl命令时忘记给整个URL加引号。比如:
curl https://example.com/api/user/query?name=zhangsan&age=25这条命令在Linux终端里会被shell拦一道。&在shell里表示"把前面的命令放到后台执行",所以shell实际会把命令拆成两条:先执行curl https://example.com/api/user/query?name=zhangsan,然后尝试在后台执行一个叫age=25的命令。这个命令大概率会报command not found。
所以,只要URL里带了&,最稳妥的做法就是给整个URL加双引号:
curl "https://example.com/api/user/query?name=zhangsan&age=25"这个坑我在无数台服务器上见过,但每次还是有人踩。
1.2 请求体不是只有POST才有,但POST的参数分工完全不同
GET请求的参数只能放在URL的查询字符串里,而POST请求的参数可以放在两个地方:URL查询字符串和请求体。
这里有个很多人没想明白的点:POST请求的URL其实也可以加?参数,相当于把一部分参数放在URL上,把另一部分放在请求体里。但在实际开发中,除非后端接口明确要求,否则不建议混着用。因为这会增加排查的复杂度:你很难一眼看出某个参数到底是通过哪种方式传过去的。
请求体的格式也分好几种,这是理解curl带参请求的关键。最常用的三种:
application/x-www-form-urlencoded:表单格式,参数以key=value&key2=value2的形式排列,这是curl用-d参数时默认使用的格式。application/json:JSON格式,整个请求体是一段JSON文本,需要显式通过-H "Content-Type: application/json"指定。multipart/form-data:文件上传格式,每个字段之间用随机边界字符串分隔,curl用-F参数时默认走这种格式。
很多人在POST请求上翻车,翻就翻在没搞清楚后端接口到底期望哪种格式。你发了一个JSON格式的请求体,但接口是按表单格式解析的,后端读出来的参数全是空。
1.3 开发、测试、运维场景下curl带参请求的实际用途
说清楚基础知识,我们再来看curl带参请求在实际工作中到底解决什么问题。至少有三类高频场景:
第一类是接口联调。前端调用后端接口报错,后端说"我这边用Postman测是好的",于是你需要用curl复现前端的环境,在Linux服务器上直接请求接口看返回结果。这个场景下,curl的每一次请求参数必须精确复现前端传参。
第二类是排查线上问题。生产环境出故障,你不能直接在服务器上打开浏览器,也没法把Postman装到生产机器上。curl是唯一依赖最少的HTTP请求工具,只要服务器上有curl,你就能快速验证接口是否存活、参数是否正确、响应是否正常。
第三类是写自动化脚本。定时任务、监控告警、数据采集脚本里,curl经常被用来调内部API。这时候请求参数往往不是写死的,而是通过变量拼接出来的,这就涉及后面要讲的批量请求和动态参数处理。
理解完这些背景,下面我们就进入正题,分别把GET和POST的带参方式细细捋一遍。
2. get请求带参的三条路:直接拼、-G配合data-urlencode、以及特殊字符转义
2.1 最直观的方式:把参数拼在URL后面
GET请求带参,最朴素的方式就是把参数直接拼在URL后面:
curl "http://example.com/api/user?id=1001&type=1"需要动态传参时,在shell脚本里可以这么写:
id=1001 type=1 curl "http://example.com/api/user?id=${id}&type=${type}"这里的${id}和${type}是shell变量,curl执行前会被替换成实际值。
这个方式的优点是直观,缺点也很明显:一旦参数里出现特殊字符,比如空格、中文、&、=、#、+等,直接拼接的URL就非常容易出现语义错误。
举个例子,如果参数值是John & Lucy,直接拼出来的URL是:
http://example.com/api/user?name=John & Lucy这个URL传过去,后端拿到的name参数大概率是John,后面的& Lucy会被当成新参数的开头,而且很可能因为格式不完整直接被解析器丢弃。
遇到这种情况,就需要手动做URL编码。John & Lucy应该被编码成John%20%26%20Lucy。手动编码既费劲又容易出错,于是就有了下面的方式。
2.2 -G 与 --data-urlencode:一行命令替代手写URL编码
curl提供了一组更聪明的参数:-G配合--data-urlencode。
curl -G "http://example.com/api/user" --data-urlencode "name=John & Lucy" --data-urlencode "age=25"-G的意思是"把-d系列参数拼到URL的查询字符串里,而不是放进请求体"。--data-urlencode则负责对参数值做URL编码。
这条命令实际发出去的URL是:
http://example.com/api/user?name=John%20%26%20Lucy&age=25有了--data-urlencode,你不需要再关心空格、&、#这些特殊字符该怎么编码,curl会帮你处理干净。
这个组合还有一个隐藏的好处:它天然绕开了shell对&的拆分问题。因为&被包含在--data-urlencode "age=25"这样的带引号参数里,shell不会把它当成后台执行符号。
在脚本里配合变量也非常方便:
name="张三 李四" age=25 curl -G "http://example.com/api/user" --data-urlencode "name=${name}" --data-urlencode "age=${age}"即便name的值里包含空格和中文,curl也能正确编码后再发送。
2.3 带空格的参数在shell里的引号陷阱
前面提到过&的坑,这里再单独强调一下空格的问题。
看这个命令:
curl "http://example.com/api/user?name=John&age=25"URL加了双引号,&没问题了。但如果参数值里本身有空格呢?
curl "http://example.com/api/user?name=John Smith&age=25"双引号把整个URL保护住了,shell不会拆词。但问题在于John Smith里的空格会原样发送给服务器,而URL里的空格在HTTP协议里是不合法的,服务器可能解析失败,或者在解析前被替换成+或%20。不同服务器的处理方式还不一样,这就增加了不确定性。
用-G加--data-urlencode就不会有这个问题:
curl -G "http://example.com/api/user" --data-urlencode "name=John Smith" --data-urlencode "age=25"curl自动把空格编码成%20,服务器能稳定解析。
还有一点容易忽略:如果参数值里包含中文,直接用普通双引号拼接URL,终端里看着没问题,但发出去的是中文字符,服务器端如果没做解码或配置了严格的字符集校验,拿到的基本都是乱码或者直接报400。--data-urlencode同样能解决中文编码问题。
所以我的习惯是:只要GET请求的参数超过一个,或者参数值里可能包含特殊字符,一律用-G加--data-urlencode,而不是手动拼URL。这个习惯帮我省掉了大量的排查时间。
3. post请求参数的不同形态:表单、JSON、文件上传,三种场景三种玩法
3.1 -d参数的本质是application/x-www-form-urlencoded
POST请求最常见的带参方式是用-d:
curl -d "name=zhangsan&age=25" "http://example.com/api/user/add"-d的全称是--data,它会把参数按照application/x-www-form-urlencoded格式放进请求体。也就是说,这条命令发出去的请求头里会自动带上Content-Type: application/x-www-form-urlencoded,请求体内容是name=zhangsan&age=25。
有几个细节值得注意:
第一,使用-d时,curl会自动把请求方法从GET切换成POST。所以-X POST经常是可以省略的。我见过不少人写curl -X POST -d "name=xxx" url,其实-X POST是多余的。
第二,多个-d参数可以叠加,curl在发送时会自动用&把它们连接起来:
curl -d "name=zhangsan" -d "age=25" "http://example.com/api/user/add"等价于:
curl -d "name=zhangsan&age=25" "http://example.com/api/user/add"第三种方式是使用--data-urlencode,和前面GET里讲的一样,会把参数值做URL编码再放进请求体:
curl --data-urlencode "name=张三" --data-urlencode "note=有空 再聊" "http://example.com/api/user/add"这个在提交中文或带空格的内容时特别好用。
3.2 JSON接口怎么发:-H指定Content-Type,-d用单引号包JSON
现在很多后端接口接收的是JSON格式。这类接口用curl发POST时,关键是要同时做两件事:
curl -H "Content-Type: application/json" -d '{"name": "张三", "age": 25}' "http://example.com/api/user/add"-H "Content-Type: application/json"告诉服务器:请求体是JSON,请按JSON来解析。-d后面跟的是一段JSON字符串,这里必须用单引号包起来,而不是双引号。
为什么必须用单引号?因为JSON字符串内部使用了双引号来标记键名和字符串值。如果外层也用双引号,shell会先对内部的双引号做解析,要么变成空字符串,要么直接报语法错误。
我也见过有人把JSON里的双引号全部转义:
curl -H "Content-Type: application/json" -d "{\"name\": \"张三\", \"age\": 25}" "http://example.com/api/user/add"这在一些终端里能跑通,但看着累,而且容易错。更推荐的做法是把JSON写进一个文件,然后让curl从文件读取:
curl -H "Content-Type: application/json" -d @user.json "http://example.com/api/user/add"其中user.json内容是:
{"name": "张三", "age": 25}注意-d @user.json里的@符号,它告诉curl"后面的内容是一个文件名,请读取文件内容作为请求体"。这在请求体很长、或者需要动态生成JSON的场景下非常实用。
如果在shell脚本里需要把变量拼进JSON,可以用这样的写法:
name="李四" age=30 curl -H "Content-Type: application/json" -d "{\"name\": \"${name}\", \"age\": ${age}}" "http://example.com/api/user/add"虽然转义看着繁琐,但这是脚本场景下的常规做法。另一种更清晰的方式是用printf或jq先生成JSON字符串再传给curl。
3.3 文件上传用-F,和-d完全不同
如果接口需要上传文件,-d就无能为力了。这时要用-F:
curl -F "file=@/path/to/photo.jpg" -F "name=test" "http://example.com/api/upload"-F表示以multipart/form-data格式提交请求体。file=@/path/to/photo.jpg里,@后的路径是本地文件路径,curl会把文件内容作为file这个字段的值发送。name=test则是普通的表单字段。
要注意的是,-F和-d的使用场景完全不同:
-d发送纯文本表单字段,不涉及文件。-F支持文件上传,也支持普通字段,两者可以混用。
用-F时,如果同时传普通字段和文件,&连接符依然有效:
curl -F "file=@./data.csv" -F "type=import" -F "desc=月度数据" "http://example.com/api/import"另外,-F同样推荐对普通字段使用--form-string来处理特殊字符。直接写-F "desc=有空 再聊",空格虽然一般没问题,但如果字段值里包含了中文或特殊符号,老老实实写成:
curl -F "file=@./data.csv" --form-string "desc=有空 再聊" "http://example.com/api/import"这样能避免很多编码问题。
4. 几个最常翻车的现场:中文乱码、&被shell吃了、调试信息怎么看
4.1 有&的参数为什么经常丢半截
这个坑算是curl带参请求里的"高频事故"了。我们在2.1里已经简单提过,这里再展开说一次。
假设你要查一个关键词,关键词本身包含&,比如AT&T。如果你写成:
curl "http://example.com/api/search?q=AT&T"注意这里的URL虽然被双引号包裹起来了,shell不会拆命令,但这个URL发送到服务器后,服务器解析查询字符串时,&还是参数分隔符。服务器眼里这个URL是嵌套的两个参数:q=AT和T=。后端读到的q值自然就变成了AT。
正确的做法是先把&做URL编码,变成%26:
curl "http://example.com/api/search?q=AT%26T"但手写编码容易漏。更建议用-G --data-urlencode:
curl -G "http://example.com/api/search" --data-urlencode "q=AT&T"这样curl自动把&编码为%26,整个搜索词完整传向后端。
同样的逻辑适用于=、%、#、+等特殊字符。在URL查询参数里,#表示锚点,+表示空格,%是转义符开头,任何一个都会干扰参数解析。所以只要参数值不确定是否包含特殊字符,就直接上--data-urlencode,别犹豫。
4.2 中文参数变成乱码的根源和规避方案
中文参数乱码,本质上还是字符编码问题。HTTP协议本身不规定字符编码,所以服务器拿到中文参数后,能否正确显示取决于:
- 客户端发送时使用的是哪种编码。
- 服务器按哪种编码解析。
curl默认发送UTF-8编码的字节内容。Linux终端下,多数现代系统默认也是UTF-8。理论上一条链路下来中文应该没问题。但实际翻车的地方往往在两层:
第一层是URL直接拼接中文。虽然curl不会把中文自动编码成%xx格式,它是"原样"发送的。某些服务器或者某些网关中间层遇到非ASCII字符,要么直接拒绝,要么转成另一种编码。所以URL里尽量不要出现裸中文,用--data-urlencode让curl编码后发送。
第二层是本地文件编码不一致。比如你在Windows上用Postman导出一个包含中文参数的curl命令,粘到Linux终端执行,终端和文件编码对不上,发送出去就是乱码。解决方法是先确认终端字符集,统一使用UTF-8,必要的时候用iconv把文件转码:
iconv -f GBK -t UTF-8 input.txt > output.txt另外,查看返回的JSON响应时,如果中文显示成\uXXXX格式,那是JSON标准里的Unicode转义,不是乱码。用jq解析一下就能显示成正常中文:
curl -G "http://example.com/api/search" --data-urlencode "q=上海" | jq .jq是一个非常轻量的JSON解析器,在Linux上配合curl用来调试HTTP接口堪称绝配。
4.3 -v和-w两个调试利器,比裸跑curl有用得多
很多人在curl报错时只会截图问人"为什么报错",但给不出任何有效信息。其实curl自带了两个非常好用的调试参数。
第一个是-v,全称--verbose。它会输出完整的HTTP请求报文和响应报文:
curl -v "http://example.com/api/user?id=1001"输出内容包含:
- 正在解析的域名和IP。
- 发起TCP连接的过程。
- TLS握手过程(如果是HTTPS)。
- 发出的请求头。
- 接收的响应头。
- 响应体。
你一眼就能看清到底请求头对不对、Content-Type有没有设置、服务器返回了什么状态码。我排查接口问题,第一步永远是加-v裸跑一次,绝大多数问题在输出里就能定位。
第二个是-w,全称--write-out,可以自定义输出格式。比如我只想看状态码和总耗时:
curl -w "状态码: %{http_code} 耗时: %{time_total}s\n" -o /dev/null "http://example.com/api/user?id=1001"这里的-o /dev/null是把响应体丢弃,因为我们只关心统计信息。-w支持很多占位符,常用的有:
%{http_code}:HTTP响应状态码。%{time_total}:总请求耗时。%{time_connect}:TCP连接耗时。%{time_namelookup}:DNS解析耗时。%{size_download}:下载的字节数。
这个用法在判断接口是否稳定、基线耗时是多少时非常高效。比如写一个循环脚本,每秒钟请求一次接口,把耗时输出到日志文件,就可以快速判断某个时段是否出现了接口变慢。
5. 进阶但很实用的组合玩法:带header请求、批量循环请求、cookie会话保持
5.1 带认证token的API请求
实际的接口联调场景里,很多接口并不是裸奔的,需要在请求头里带认证信息。最常见的是Bearer Token:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." "http://example.com/api/user/info"-H可以多次使用,一次请求带多个头部:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." -H "Accept: application/json" -H "X-Request-Id: 20240601" "http://example.com/api/user/info"这里X-Request-Id是自定义头部,通常用来做链路追踪。后端可以根据这个ID在日志里定位到这次请求。
带参数的POST加header是这么组合的:
curl -X POST \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \ -H "Content-Type: application/json" \ -d '{"name": "张三", "age": 25}' \ "http://example.com/api/user/add"注意这里用\做了命令换行。在终端里如果一行太长,用反斜杠换行可以让命令清晰很多。
对于需要签名的API,通常做法是先算出签名值,再拼到某个Header或URL参数里:
sign=$(echo -n "timestamp=1717210000&key=secret" | md5sum | awk '{print $1}') curl -H "X-Sign: ${sign}" -H "X-Timestamp: 1717210000" "http://example.com/api/data"签名计算方式因接口而异,但思路是一致的:把参数按照约定的规则排序拼接,算出签名,然后在请求时带上,让服务端做同样的计算并比对。这种场景在开放平台对接时尤其常见。
5.2 用一个for循环批量给接口灌不同参数
拿到一个分页接口,想验证第一页到第五页返回的数据是否正常,不用一条条手动写curl,直接在shell里循环:
for page in 1 2 3 4 5; do echo "=== 第 ${page} 页 ===" curl -G "http://example.com/api/list" --data-urlencode "page=${page}" --data-urlencode "size=20" echo done如果需要测试的参数更加多样,可以定义一个二维数组。比如测不同搜索关键词:
keywords=("苹果" "华为" "小米" "三星") for kw in "${keywords[@]}"; do echo "=== 搜索: ${kw} ===" curl -s -G "http://example.com/api/search" --data-urlencode "keyword=${kw}" echo done这里的-s是--silent,静默模式。不加-s时curl会显示进度条,在脚本和循环里进度条会刷屏,非常影响输出阅读。
还有一种场景是接口压测。虽然专业的压测工具是ab或wrk,但如果只是快速验证TPS和响应时间,可以用curl配合并发小技巧:
seq 1 20 | xargs -P 5 -I {} curl -s -o /dev/null -w "%{http_code} %{time_total}\n" "http://example.com/api/ping"这个命令用xargs -P 5实现5个并发请求,输出每个请求的状态码和耗时。这在接口改动后快速验收时非常方便,不需要引入额外工具。
5.3 cookie与会话保持:-c和-b的配合
有些接口需要登录态,登录后服务端会在响应里返回一个Set-Cookie头,后续请求必须携带这个Cookie才能通过认证。
curl提供两个参数配合处理这种场景:
-c:把响应里的Cookie写入指定文件。-b:从指定文件读取Cookie并发送。
典型的流程是先登录,保存Cookie:
curl -c cookies.txt -d "username=admin&password=123456" "http://example.com/api/login"然后带着Cookie访问需要登录态的接口:
curl -b cookies.txt "http://example.com/api/user/info"用-v看请求头,你会发现Cookie字段已经被自动带上了。这在调试需要登录态的接口时非常管用,不用每次都在命令里手动拼Cookie字符串。
如果你已经拿到了浏览器里的Cookie值,也可以直接用-b传字符串:
curl -b "sessionid=abc123; token=xyz789" "http://example.com/api/user/info"多个Cookie用;分隔,注意要用引号包起来,防止;被shell解释成命令分隔符。
实际工作中我发现,很多人都会忽略-c和-b这两个参数的存在,遇到Cookie场景就在命令行里手动复制粘贴Cookie字符串,不但容易粘贴不全,还会因为格式错误导致认证失败。用文件保存的方式,既干净又可复用。
5.4 安全地处理HTTPS证书报错
最后提一个几乎所有人在实际环境里都会遇到的问题:用curl访问HTTPS接口时,偶尔会报SSL certificate problem错误。
这个报错的原因通常是服务器使用了自签名证书或者证书链不完整。在测试环境里,一个快速绕过校验的方式是加-k:
curl -k "https://self-signed.example.com/api/ping"-k是--insecure的简写,作用是跳过证书校验。但这个做法只能用于测试环境。生产环境的接口,如果证书校验失败,应该先排查是不是证书真的过期了或配置错了,而不是直接跳过校验。
另一个常被误解的参数是-L(--location)。它让curl在收到301/302重定向时自动跳转:
curl -L "http://example.com/start"有些接口会把HTTP请求重定向到HTTPS,如果不加-L,curl只会拿到一个重定向响应,看不到真实的内容。加了-L才会自动跟随过去。
还有几个脚本开发常用的参数组合,顺手一起说了:
-f:服务器返回4xx/5xx状态码时,让curl返回错误码而不是打印错误页面。配合-s使用可以避免脚本里出现大量无意义的HTML。-sS:静默但是显示错误。-s本身连错误都不显示,组合成-sS后,进度条隐藏了,错误信息还能看到。--connect-timeout:指定连接超时时间(秒),防止接口连不上时curl一直卡着。-m:整体请求的最大耗时限制,超过就杀掉请求。
典型的组合是:
curl -fsSL --connect-timeout 5 -m 10 "http://example.com/api/ping" || echo "请求失败"这个写法在很多一键安装脚本里都很常见,-f确保失败时不输出垃圾内容,-s保证安静,-S允许显示错误,-L跟随重定向,超时控制保证整个流程不会卡死。
在我实际使用curl的过程中,最有价值的一个习惯是:所有参数值不确定的命令,一律优先使用--data-urlencode;所有需要排查的问题,一律先加-v跑一次;所有会重复使用的请求,一律写成脚本而不是手动敲。把这三个习惯坚持下来,curl带参请求的日常坑基本能避开九成。