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

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

**快速答案:** 一条 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 命令不是一整块字符串，而是一组参数：

```bash
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 参数 | 含义 | fetch | axios |
| --- | --- | --- | --- |
| `-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: fileBlob` | `data: fileBlob` |
| `-F 'name=value'` | 表单字段 | `FormData` | `FormData` |
| `-u user:pass` | Basic 认证 | `Authorization` 头 | `auth: { username, password }` |
| `-b 'name=value'` | Cookie | `credentials` 或 Cookie 头 | `withCredentials` 或头 |
| `-L` | 跟随重定向 | `redirect: "follow"`（默认） | 默认跟随 |
| `--json '{...}'` | JSON 正文加 JSON 内容类型（curl 7.82+） | `body: JSON.stringify(...)` 加头 | `data: {...}` |
| `-G` | 把数据移到查询串 | 用 `URLSearchParams` 拼接 | `params` |
| `--data-urlencode` | 对表单字段做 URL 编码 | `URLSearchParams` | `params` |
| `-I` | HEAD 请求 | `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

```bash
curl 'https://api.example.com/orders?status=open' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer TOKEN'
```

```js
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 版本：

```js
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

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

```js
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`：

```js
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 编码转换工具](/tools/form-urlencoded) 可以检查编码结果。

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 认证

```bash
curl -u 'user:password' 'https://api.example.com/me'
```

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

axios：

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

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

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

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

## 文件上传

```bash
curl -X POST 'https://api.example.com/upload' \
  -F 'file=@report.pdf' \
  -F 'description=Monthly report'
```

```js
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`；不要假设浏览器代码在服务端能原样运行，先测运行时。

## 重定向与 Cookie

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 请求工作台](/tools/http-request) 或 [cURL 转换工具](/tools/curl-converter) 检查。

重点对比：

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。

## 参考资料

- [curl 手册](https://curl.se/docs/manpage.html)
- [MDN - 使用 Fetch API](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch)
- [MDN - FormData](https://developer.mozilla.org/zh-CN/docs/Web/API/FormData)
- [axios - 请求配置](https://axios-http.com/docs/req_config)

---

- Canonical: https://www.jsonmind.com/zh-CN/guides/convert-curl-to-fetch-axios
- Locale: zh-CN
- Published: 2026-11-09
- Updated: 2026-11-09
- Tools: curl-converter, http-request, form-urlencoded, basic-auth