Fetch API

浏览器原生的网络请求接口,以 Request、Response、Headers 和 Stream 抽象请求与响应。

#type / concept #status / growing #tech / network #platform / browser #resource / http #resource / javascript

[!info] related notes

Fetch API

一句话定义

fetch() 是浏览器和 Worker 环境中的原生网络请求接口。它返回的不是“数据”,而是一个会在响应可用时兑现为 Response 对象的 Promise。

正确心智模型

很多人把 fetch 理解成“发请求然后拿 JSON”,这不够准确。fetch 更像一套请求/响应抽象:

  • fetch(input, init):发起请求
  • Request:描述我要发什么请求
  • Response:描述服务器回了什么
  • Headers:请求头和响应头容器
  • body:请求体或响应体,本质上是

因此:

const response = await fetch("/api/user");
console.log(response); // Response

上面拿到的是 Response 外壳,不是最终业务数据。真正的数据还要继续读取:

const response = await fetch("/api/user");
const data = await response.json();

最关键的语义

await fetch() 不等于拿到最终数据

fetch() 的 Promise 兑现后,你只是拿到了 Response。读取 body 仍然要额外调用:

  • response.json()
  • response.text()
  • response.blob()
  • response.arrayBuffer()

HTTP 404/500 默认不会抛错

fetch() 只会在请求本身失败时 reject,例如:

  • 网络错误
  • URL 非法
  • 请求被浏览器拦截
  • AbortController 取消

如果服务器正常返回了一个 HTTP 响应,即使状态码是 404500504,Promise 仍然会 resolve,所以要自己检查:

const response = await fetch("/api/user");

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

const data = await response.json();

这也是 fetch 和很多人直觉不一致的地方:

“拿到了 HTTP 响应”
不等于
“业务上成功了”

基础用法

GET

async function getUsers() {
  const response = await fetch("/api/users");

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return response.json();
}

POST

async function createUser(user) {
  const response = await fetch("/api/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(user)
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return response.json();
}

fetch(input, init) 在做什么

常见签名是:

fetch(input, init);
  • input 可以是 URL,也可以是 Request
  • initRequestInit

常见选项有:

  • method
  • headers
  • body
  • signal
  • mode
  • credentials
  • cache
  • redirect
  • referrerPolicy
  • keepalive

如果同一个选项同时出现在 Requestinit 中,fetch() 里直接传入的值优先。

headers 不是普通对象

可以直接传对象:

headers: {
  "Content-Type": "application/json"
}

也可以显式使用 Headers

const headers = new Headers();
headers.append("Content-Type", "application/json");

Headers 不只是 key-value 容器,它还带有浏览器侧的规范化和限制:

  • header 名会被规范化
  • 值会做基础清洗
  • 某些受限制的请求头不能由脚本自由设置

所以浏览器环境里不是“想设什么 header 都能设什么”。

body 的本质是流,而且通常只能消费一次

fetch 的请求体和响应体都带有流语义。无论是发送请求还是读取响应,都会消费 body。

例如:

const request = new Request("/api/post", {
  method: "POST",
  body: JSON.stringify({ hello: "world" })
});

await fetch(request);
await fetch(request); // Body has already been consumed

如果确实要复用,需要先克隆:

const request = new Request("/api/post", {
  method: "POST",
  body: JSON.stringify({ hello: "world" })
});

const copy = request.clone();

await fetch(request);
await fetch(copy);

响应体同样如此:

const response = await fetch("/api/user");
const data = await response.json();
// await response.text(); // 通常会失败

如何读取 Response

常见读取方式:

  • response.json():读完整个 body,再按 JSON 解析
  • response.text():读成字符串
  • response.blob():适合图片、文件等二进制对象
  • response.arrayBuffer():适合更底层的二进制处理

json() 返回的是解析后的 JavaScript 值,不是 JSON 字符串。

const response = await fetch("/api/user");
const user = await response.json();

一个常见坑是:服务端如果返回的是 HTML 报错页、半截字符串、或非法 JSON,response.json() 会抛解析错误。工程上更稳的做法是先判断 Content-Type

const response = await fetch("/api/data");
const contentType = response.headers.get("content-type") || "";

const payload = contentType.includes("application/json")
  ? await response.json()
  : await response.text();

bodyUsedclone() 与“一次性读取”

Response.bodyUsed 表示 body 是否已经被消费过。

const response = await fetch("/api/user");
console.log(response.bodyUsed); // false

await response.json();
console.log(response.bodyUsed); // true

如果确实需要读取两次,要在第一次读取前 clone()

const response = await fetch("/api/user");
const copy = response.clone();

const data = await response.json();
const raw = await copy.text();

clone() 并不是免费操作。对于大响应体,如果两个副本消费速度差异很大,慢的一侧可能积压更多内存。

跨域、modecredentials

fetch 的“跨域问题”本质上不是语法问题,而是浏览器的同源策略和 CORS 机制在起作用。见:

mode

常见模式:

  • cors:常规跨源请求模式,服务器允许时前端可读取响应
  • same-origin:只允许同源
  • no-cors:不是绕过跨域,而是发出受限请求,前端通常只能拿到 opaque 响应

所以:

fetch(url, { mode: "no-cors" })

通常不是解决跨域的正确方式。

credentials

credentials 决定是否携带 cookie 或其他凭证:

  • omit
  • same-origin
  • include

跨源请求即使写了 credentials: "include",也还需要服务端同时正确返回 CORS 凭证相关头部;否则浏览器不会把响应正常暴露给前端代码。

取消请求与超时

fetch 没有内建 timeout 参数。标准做法是 AbortController

async function fetchWithTimeout(url, init = {}, timeout = 8000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    return await fetch(url, {
      ...init,
      signal: controller.signal
    });
  } finally {
    clearTimeout(timer);
  }
}

如果被取消,通常会抛出 AbortError

try {
  await fetchWithTimeout("/api/slow");
} catch (error) {
  if (error.name === "AbortError") {
    console.log("request aborted");
  }
}

fetch 很强的一面:响应体可以流式读取

Response.bodyReadableStream。这意味着你不一定要等整个响应体下载完成后再处理,适合:

  • AI 输出流
  • 大文件下载
  • 日志流
  • 渐进式文本展示
const response = await fetch("/api/stream");
const textStream = response.body.pipeThrough(new TextDecoderStream());
const reader = textStream.getReader();

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  console.log(value);
}

这部分属于更广义的流式处理,展开看:

进阶选项

keepalive

适合页面卸载前上报少量数据,例如埋点或退出事件。

fetch("/analytics", {
  method: "POST",
  body: JSON.stringify({ action: "leave" }),
  headers: {
    "Content-Type": "application/json"
  },
  keepalive: true
});

duplex

它和流式请求体上传有关,属于更前沿、兼容性也更挑环境的能力。理解它即可,生产里要谨慎看兼容性。

常见误区

  • await fetch() 不等于拿到业务数据,只是拿到 Response
  • 404500 默认不会进 catch
  • mode: "no-cors" 不是“绕过跨域”开关
  • 一个 response 不能先 json()text() 随便读两次
  • credentials: "include" 不代表跨域 cookie 一定能成功带上

和 Axios 的边界

fetch 是浏览器原生 API;Axios 是额外封装过的库。两者的选型、错误语义差异和工程实践建议,放在关系笔记里统一看更清楚:

创建于 2025/12/8 更新于 2026/5/27