JsonMind
API 与接口8 分钟阅读更新于 2026年11月9日

curl 转 fetch 或 axios:参数映射与常见坑

把 curl 的 -X、-H、-d、-F、-u 等参数准确映射到 fetch 和 axios,避免 Content-Type、重定向、错误处理和 CORS 问题。

JM作者:JsonMind Engineering最后复核: 2026年11月9日依据权威规范与引用来源复核。
快速答案

一条 curl 命令本质上是一组请求参数:方法、URL、请求头、正文、认证和重定向策略。转换时把每个参数映射到 fetch 或 axios 的对应选项,然后用请求检查工具对比两边实际发出的头和正文,而不是只看代码像不像。

核心要点
  • curl -d 会发送正文,并默认使用 application/x-www-form-urlencoded;JSON 需要显式设置 Content-Type。
  • fetch 不会因为 4xx 或 5xx 而 reject,必须自己检查 response.ok。
  • 浏览器 fetch 会执行 CORS,curl 和服务端 fetch 不会。
  • axios 传入对象时会自动序列化 JSON。
  • 转换完成后要对比实际请求,而不是只对比代码。

从接口文档复制一条 curl 命令很容易,把它准确转成 fetch 或 axios 才是细节所在:Content-Type、正文编码、认证、重定向和错误处理,任何一项不同都会导致结果不同。

这篇文章把常用参数逐项映射,并说明如何验证转换是否正确。

把 curl 命令拆成请求参数

curl 命令不是一整块字符串,而是一组参数:

curl -X POST 'https://api.example.com/orders' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer TOKEN' \
  -d '{"item":"keyboard","quantity":2}'

它对应的请求是:

  • 方法:POST
  • URL:https://api.example.com/orders
  • 请求头:Content-Type: application/json、Authorization: Bearer TOKEN
  • 正文:{"item":"keyboard","quantity":2}

把这四件事列清楚,JavaScript 版本就很容易写。

四行摘要还会漏掉一些会改变请求的信息:没有 -X 时 -d 隐含 POST,-I 隐含 HEAD,-G 会把数据移到查询串而不是正文。还有一些参数根本无法映射到浏览器 fetch:--resolve、--cacert、--cert、--proxy、--http1.1、--connect-timeout 描述的是传输层,不是 HTTP 请求本身,它们属于服务端客户端或代理。默默丢掉这些参数,就是“curl 能跑、浏览器跑不通”的常见起点。

shell 引号也是请求的一部分。单引号会保护 $ 和 !,双引号会做变量展开,所以文档里含 $TOKEN 的命令,可能已经在文档作者的环境里替换过,也可能在你的环境里展开成空字符串。-d '...' 里的多行 JSON 之所以保留换行,是引号的作用;重新手打时如果去掉引号,正文就变了。

参数映射表

curl 参数含义fetchaxios
-X POST请求方法method: "POST"method: "post"
-H 'Name: value'请求头headers: { Name: "value" }headers: { Name: "value" }
-d '...'请求正文body: "..."data: "..."
--data-raw原样正文body: "..."data: "..."
--data-binary @file二进制正文body: fileBlobdata: fileBlob
-F 'name=value'表单字段FormDataFormData
-u user:passBasic 认证Authorization 头auth: { username, password }
-b 'name=value'Cookiecredentials 或 Cookie 头withCredentials 或头
-L跟随重定向redirect: "follow"(默认)默认跟随
--json '{...}'JSON 正文加 JSON 内容类型(curl 7.82+)body: JSON.stringify(...) 加头data: {...}
-G把数据移到查询串用 URLSearchParams 拼接params
--data-urlencode对表单字段做 URL 编码URLSearchParamsparams
-IHEAD 请求method: "HEAD"method: "head"
-m 10整个传输的超时signal: AbortSignal.timeout(10000)timeout: 10000 或 signal
--retry 3失败自动重试自己实现带退避的循环用拦截器实现
--compressed请求压缩响应自动处理自动处理

两个映射要特别小心。--data-urlencode 会对每个字段做表单编码,fetch 没有对应参数,应该构造 URLSearchParams,而不是手工把字符串拼到 URL 上。-m 10 限制整个传输,Axios 的 timeout 在不同 adapter 下语义不完全一致;需要精确截止时间时,fetch 和 Axios 都用 AbortSignal.timeout。--retry 完全没有直接对应物,重试策略还必须知道请求是否幂等:重试 POST 可能创建重复订单,所以只对安全方法或支持幂等键的接口启用,并加抖动退避。

带请求头的 GET

curl 'https://api.example.com/orders?status=open' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer TOKEN'
const response = await fetch("https://api.example.com/orders?status=open", {
  method: "GET",
  headers: {
    Accept: "application/json",
    Authorization: "Bearer TOKEN"
  }
});

if (!response.ok) {
  throw new Error(`请求失败:${response.status} ${await response.text()}`);
}

const data = await response.json();

axios 版本:

const { data } = await axios.get("https://api.example.com/orders", {
  params: { status: "open" },
  headers: { Authorization: "Bearer TOKEN" }
});

axios 会把 params 拼成查询字符串;fetch 需要用 URLSearchParams 自己拼。

查询串的细节容易造成静默偏差。URLSearchParams 把空格编码成 +,这对表单编码是正确的,但期待 %20 的接口可能不认。重复键必须用 append:对象字面量无法表示 status=open&status=paid,后一个值会覆盖前一个。Axios 默认把数组序列化成 status[]=open,接口如果期待重复的裸键,需要自定义 paramsSerializer。

fetch 也不允许 GET 或 HEAD 带正文:new Request(url, { method: "GET", body: "..." }) 会抛 TypeError: Request with GET/HEAD method cannot have body。curl 会老老实实发送 -X GET -d '...',浏览器 API 不会,这种请求需要重新设计,而不是换个参数。

POST JSON

curl -X POST 'https://api.example.com/orders' \
  -H 'Content-Type: application/json' \
  -d '{"item":"keyboard","quantity":2}'
const response = await fetch("https://api.example.com/orders", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ item: "keyboard", quantity: 2 })
});

axios 传入对象时会自动设置 JSON 内容类型;fetch 需要自己设置头并序列化正文。

JSON.stringify 不是对 curl 正文的透明复制。值为 undefined 的属性会消失,Date 会通过 toJSON 变成 ISO 字符串,BigInt 会抛 TypeError: Do not know how to serialize a BigInt,循环引用会抛 Converting circular structure to JSON。如果原始正文是手写的,序列化后的空白、重建对象后的键顺序、非 ASCII 的转义方式都可能不同。普通接口不在意,签名方案会在意。

Axios 只在它自己序列化对象时自动加 JSON 内容类型;传入已经序列化好的字符串时,必须显式设置请求头,否则请求可能以 text/plain 发出,内容协商直接失败。

Content-Type 陷阱

curl -d 默认使用 application/x-www-form-urlencoded,不是 JSON。如果命令里有 -H 'Content-Type: application/json',以显式头为准;如果没有,服务端收到的就是表单编码。

表单编码请求应该使用 URLSearchParams:

const body = new URLSearchParams({
  grant_type: "client_credentials",
  client_id: "abc",
  client_secret: "secret"
});

const response = await fetch("https://auth.example.com/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body
});

表单 URL 编码转换工具 可以检查编码结果。

curl 的 -d @file 读取文件时会去掉换行和回车,--data-binary @file 才保留原始字节。格式化过的 JSON 用 -d @payload.json 发送,到达服务端时变成一行,通常无害;但依赖换行的内容(NDJSON、签名、PEM 块)会被破坏。文件字节必须原样保留时,用 --data-binary。

URLSearchParams 作为 fetch 正文时会自动设置 Content-Type: application/x-www-form-urlencoded;charset=UTF-8。手动设置一个不带 charset 的头会覆盖它,有些服务端对 charset 参数很严格;要么让对象自己设置,要么把参数写全。代理和应用各加一个 Content-Type 时,重复头也会导致 415。

Basic 认证

curl -u 'user:password' 'https://api.example.com/me'
const credentials = btoa("user:password");
const response = await fetch("https://api.example.com/me", {
  headers: { Authorization: `Basic ${credentials}` }
});

axios:

const { data } = await axios.get("https://api.example.com/me", {
  auth: { username: "user", password: "password" }
});

浏览器里的 btoa 处理不了非拉丁字符。应该先把凭据编码成 UTF-8 字节。Basic 认证头生成工具 会处理这个问题。

RFC 7617 在解码后的凭据里按第一个冒号切分:用户名不能包含冒号,密码可以。用户名里带冒号时,服务端会解析成另一个用户,或者直接认证失败。非 ASCII 凭据没有统一字符集,标准允许 charset 参数;没有该参数时,有的服务端按 UTF-8,有的按 Latin-1。接口文档指定了字符集就按它编码,不要假设 btoa 已经处理正确。

浏览器里的 Basic 认证对用户和任何能读取页面的脚本都可见,它不是密钥存储机制,而且每次请求都会带上密码。只在 TLS 上使用,接口支持短期 Token 时优先用 Token。

文件上传

curl -X POST 'https://api.example.com/upload' \
  -F 'file=@report.pdf' \
  -F 'description=Monthly report'
const form = new FormData();
form.append("file", fileInput.files[0]);
form.append("description", "Monthly report");

const response = await fetch("https://api.example.com/upload", {
  method: "POST",
  body: form
});

不要手动设置 Content-Type。浏览器会自动加上正确的 multipart boundary;手动设置通常会让服务端无法解析。

-F 携带的信息比字段名多。-F 'file=@report.pdf;type=application/pdf' 设置分片的 content type,-F 'file=@photo.jpg;filename=renamed.jpg' 覆盖服务端看到的文件名。在 fetch 里,form.append("file", blob, "renamed.jpg") 的第三个参数就是文件名;File 自带 type,普通 Blob 可能需要在构造时显式指定。同名字段上传多个文件,就是重复调用 append。

fetch 无法报告上传进度。界面需要进度条时,用 XMLHttpRequest 或 Axios 的 onUploadProgress(浏览器里底层就是 XHR)。Node 里的 Axios 可能需要 form-data 包,或者依赖运行时提供的全局 FormData;不要假设浏览器代码在服务端能原样运行,先测运行时。

fetch 默认跟随重定向,但跨域重定向和携带凭据的请求行为不同。如果原 curl 依赖 -L 和 Cookie,需要逐项确认:

  • 浏览器 fetch:跨域 Cookie 使用 credentials: "include"。
  • Node fetch:不会自动管理 Cookie,需要手动传 Cookie 头。
  • 浏览器 axios:withCredentials: true。
  • Node axios:使用 cookie jar 或手动传头。

fetch 的 redirect 有三个值:follow(默认)、error、manual。浏览器里 manual 返回 opaque-redirect,状态是 0,读不到 Location;Node 的 fetch 实现里 manual 会暴露 3xx 响应。需要读取跳转目标的转换,可能在 Node 通过、在浏览器失败。fetch 还会限制跳转次数,超过限制时以 TypeError reject,而不是返回最后一个响应。

Cookie 不属于 fetch API。浏览器里 credentials: "include" 让浏览器按自己的 Cookie 策略携带和保存 Cookie,但 JavaScript 仍然读不到 Set-Cookie。Node 的 fetch 完全没有 cookie jar,登录流程需要手动处理请求头,或者用带 jar 的客户端库。Node 里的 Axios 同样如此,withCredentials 只对浏览器生效。

如何验证转换正确

最可靠的方法是对比两边实际发出的请求。可以用 HTTP 请求工作台 或 cURL 转换工具 检查。

重点对比:

  1. 方法和 URL,包括查询参数。
  2. 每个请求头,尤其是 Content-Type 和 Authorization。
  3. 正文字节。JSON 键顺序通常不重要,但编码和空白可能重要。
  4. 重定向行为。
  5. 错误处理。fetch 返回 401 时仍然是 resolve,不是成功。

转换正确的标准是服务端收到同样的请求,而不是代码看起来相似。

请求带签名时,字节级一致性是硬要求。AWS Signature V4 和 webhook HMAC 都会对包含正文哈希的规范请求签名;用 JSON.stringify 重建正文可能改变键顺序或转义,JSON 语义相同,签名却对不上。这种场景要保留原始正文字符串,对那串字节签名。

Chrome 和 Firefox 的网络面板可以把请求导出成 cURL(Copy as cURL),这是对比浏览器版本和原始命令最快的方法。curl 的 --trace-ascii 会打印收发的字节和请求头,涉及空白或编码时比 -v 更精确。

浏览器与服务端的区别

curl 通常在浏览器之外运行。如果转成浏览器端 fetch,会多出一些限制:

  • 非简单请求会触发 CORS 预检。
  • 不能读取某些禁止的头。
  • 携带凭据需要显式配置。
  • 混合内容会被拦截。

禁止头列表解释了不少失败案例。浏览器 fetch 会静默丢弃对 Host、Cookie、Content-Length、Origin、Referer 的设置。服务端如果要求手工计算的 Content-Length 或特定的 Cookie 头,浏览器 JavaScript 根本做不到,只能把请求移到服务端。

最后一条约束是保密。文档里的 curl 命令带 API key,是因为读者会在可信机器上运行;把同一个 key 放进浏览器代码,等于向每个访客公开。使用密钥的接口应该由后端调用,浏览器只调用自己的后端。能跑通但把密钥发到前端的转换,仍然是错误的架构。

如果接口并不是为浏览器设计的,应该把请求留在服务端,只暴露一个窄接口给前端。不要为了跑通复制来的命令而关闭浏览器安全策略。

常见问题

为什么转换后的 fetch 请求返回结果不一样?

最常见的原因是缺少 Content-Type、正文序列化方式不同、漏了 Authorization 头,或者 curl 跟随了重定向而 fetch 没有。应该逐项对比请求头、请求体和重定向行为。

fetch 遇到 500 会抛错吗?

不会。只要网络层成功,fetch 对 4xx 和 5xx 都会正常 resolve。需要检查 response.ok 或 response.status,并显式读取错误正文。

curl -u user:password 怎么转成 fetch?

把用户名和密码用冒号拼接,做 Base64 编码,然后作为 Authorization: Basic <值> 发送。不要把凭据直接放进 URL。

为什么 curl 成功,浏览器 fetch 却报 CORS?

curl 不执行浏览器的同源策略。浏览器对很多跨域请求会先发预检,服务端没有返回正确的 CORS 头时就会拦截。需要改服务端配置,而不是改 fetch 写法。

fetch 怎么上传文件?

使用 FormData,把文件和普通字段 append 进去。不要手动设置 Content-Type,浏览器会自动加上带 boundary 的 multipart/form-data。axios 也接受同样的 FormData。

参考资料

把指南用起来

浏览全部工具

打开浏览器工具。输入内容保留在浏览器中,不会上传到业务服务器。

相关指南

全部指南