Skip to content

Bun Fetch API

Bun 内置了符合 Web 标准的 Fetch API,用于发送 HTTP 请求。本章介绍 Bun 的网络请求功能。

基本请求

GET 请求

typescript
// 最简单的 GET 请求
const response = await fetch("https://api.example.com/users");
const data = await response.json();
console.log(data);

// 获取文本
const text = await fetch("https://example.com").then(r => r.text());

// 获取二进制数据
const buffer = await fetch("https://example.com/image.png")
  .then(r => r.arrayBuffer());

POST 请求

typescript
// 发送 JSON
const response = await fetch("https://api.example.com/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "张三",
    email: "zhangsan@example.com",
  }),
});

const result = await response.json();
console.log(result);

其他 HTTP 方法

typescript
// PUT 请求
await fetch("https://api.example.com/users/1", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "新名字" }),
});

// PATCH 请求
await fetch("https://api.example.com/users/1", {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "new@example.com" }),
});

// DELETE 请求
await fetch("https://api.example.com/users/1", {
  method: "DELETE",
});

请求配置

完整配置选项

typescript
const response = await fetch("https://api.example.com/data", {
  // HTTP 方法
  method: "POST",
  
  // 请求头
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer your-token",
    "Accept": "application/json",
    "User-Agent": "Bun/1.0",
  },
  
  // 请求体
  body: JSON.stringify({ data: "value" }),
  
  // 重定向处理
  redirect: "follow", // "follow" | "error" | "manual"
  
  // 凭证
  credentials: "include", // "omit" | "same-origin" | "include"
  
  // 缓存模式
  cache: "no-cache",
  
  // 超时(毫秒)- Bun 特有
  timeout: 30000,
  
  // TLS 配置 - Bun 特有
  tls: {
    rejectUnauthorized: true,
  },
});

请求头

typescript
// 使用 Headers 对象
const headers = new Headers();
headers.set("Content-Type", "application/json");
headers.set("Authorization", "Bearer token");
headers.append("Accept-Language", "zh-CN");

const response = await fetch("https://api.example.com", { headers });

// 检查响应头
console.log(response.headers.get("content-type"));
console.log(response.headers.get("x-request-id"));

// 遍历响应头
for (const [key, value] of response.headers) {
  console.log(`${key}: ${value}`);
}

响应处理

Response 对象

typescript
const response = await fetch("https://api.example.com/data");

// 响应状态
console.log("状态码:", response.status);         // 200
console.log("状态文本:", response.statusText);   // "OK"
console.log("是否成功:", response.ok);           // true (200-299)

// 响应 URL
console.log("最终 URL:", response.url);

// 响应类型
console.log("类型:", response.type);             // "basic" | "cors" | etc.

// 是否重定向
console.log("已重定向:", response.redirected);

读取响应体

typescript
const response = await fetch("https://api.example.com/data");

// JSON
const json = await response.json();

// 文本
const text = await response.text();

// ArrayBuffer
const buffer = await response.arrayBuffer();

// Blob
const blob = await response.blob();

// FormData
const formData = await response.formData();

// 注意:响应体只能读取一次
// 如果需要多次使用,先克隆
const clone = response.clone();
const text1 = await response.text();
const text2 = await clone.text();

流式读取

typescript
const response = await fetch("https://example.com/large-file");

// 获取可读流
const reader = response.body?.getReader();

if (reader) {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    console.log("读取块:", value.length, "字节");
  }
}

表单和文件上传

FormData

typescript
const formData = new FormData();
formData.append("name", "张三");
formData.append("age", "25");

const response = await fetch("https://api.example.com/submit", {
  method: "POST",
  body: formData,
  // 不需要手动设置 Content-Type,fetch 会自动处理
});

文件上传

typescript
// 上传本地文件
const file = Bun.file("./document.pdf");

const formData = new FormData();
formData.append("file", file);
formData.append("description", "重要文档");

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

console.log("上传结果:", await response.json());

多文件上传

typescript
const formData = new FormData();

// 添加多个文件
formData.append("files", Bun.file("./image1.png"));
formData.append("files", Bun.file("./image2.png"));
formData.append("files", Bun.file("./image3.png"));

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

错误处理

基本错误处理

typescript
try {
  const response = await fetch("https://api.example.com/data");
  
  if (!response.ok) {
    throw new Error(`HTTP 错误: ${response.status}`);
  }
  
  const data = await response.json();
  return data;
} catch (error) {
  if (error instanceof TypeError) {
    console.error("网络错误:", error.message);
  } else {
    console.error("请求失败:", error);
  }
}

超时处理

typescript
// 方法 1: 使用 Bun 的 timeout 选项
const response = await fetch("https://api.example.com/slow", {
  timeout: 5000, // 5 秒超时
});

// 方法 2: 使用 AbortController
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch("https://api.example.com/slow", {
    signal: controller.signal,
  });
  clearTimeout(timeout);
  return await response.json();
} catch (error) {
  if (error.name === "AbortError") {
    console.error("请求超时");
  }
  throw error;
}

取消请求

typescript
const controller = new AbortController();

// 开始请求
const fetchPromise = fetch("https://api.example.com/data", {
  signal: controller.signal,
});

// 在某个条件下取消
setTimeout(() => {
  controller.abort();
  console.log("请求已取消");
}, 1000);

try {
  const response = await fetchPromise;
} catch (error) {
  if (error.name === "AbortError") {
    console.log("请求被取消");
  }
}

重试机制

简单重试

typescript
async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  maxRetries = 3
): Promise<Response> {
  let lastError: Error | null = null;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      
      if (response.ok) {
        return response;
      }
      
      // 服务器错误,可以重试
      if (response.status >= 500) {
        lastError = new Error(`Server error: ${response.status}`);
        continue;
      }
      
      // 客户端错误,不重试
      return response;
    } catch (error) {
      lastError = error as Error;
      console.log(`尝试 ${i + 1} 失败:`, error);
      
      // 等待后重试
      await Bun.sleep(1000 * (i + 1));
    }
  }
  
  throw lastError || new Error("请求失败");
}

// 使用
const response = await fetchWithRetry("https://api.example.com/data");

指数退避

typescript
async function fetchWithExponentialBackoff(
  url: string,
  options: RequestInit = {},
  maxRetries = 5
): Promise<Response> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      if (response.ok) return response;
      if (response.status < 500) return response;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
    }
    
    // 指数退避: 1s, 2s, 4s, 8s, 16s
    const delay = Math.min(1000 * Math.pow(2, i), 30000);
    const jitter = Math.random() * 1000;
    await Bun.sleep(delay + jitter);
  }
  
  throw new Error("Max retries exceeded");
}

HTTP 客户端封装

API 客户端

typescript
class ApiClient {
  private baseUrl: string;
  private headers: Record<string, string>;

  constructor(baseUrl: string, headers: Record<string, string> = {}) {
    this.baseUrl = baseUrl;
    this.headers = {
      "Content-Type": "application/json",
      ...headers,
    };
  }

  setHeader(key: string, value: string) {
    this.headers[key] = value;
  }

  setToken(token: string) {
    this.headers["Authorization"] = `Bearer ${token}`;
  }

  private async request<T>(
    method: string,
    path: string,
    body?: unknown
  ): Promise<T> {
    const response = await fetch(`${this.baseUrl}${path}`, {
      method,
      headers: this.headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      throw new ApiError(response.status, error.message || response.statusText);
    }

    return response.json();
  }

  get<T>(path: string): Promise<T> {
    return this.request<T>("GET", path);
  }

  post<T>(path: string, body: unknown): Promise<T> {
    return this.request<T>("POST", path, body);
  }

  put<T>(path: string, body: unknown): Promise<T> {
    return this.request<T>("PUT", path, body);
  }

  patch<T>(path: string, body: unknown): Promise<T> {
    return this.request<T>("PATCH", path, body);
  }

  delete<T>(path: string): Promise<T> {
    return this.request<T>("DELETE", path);
  }
}

class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = "ApiError";
  }
}

// 使用
const api = new ApiClient("https://api.example.com");
api.setToken("your-auth-token");

const users = await api.get<User[]>("/users");
const newUser = await api.post<User>("/users", { name: "张三" });

并发请求

Promise.all

typescript
// 并行请求多个 API
const [users, posts, comments] = await Promise.all([
  fetch("https://api.example.com/users").then(r => r.json()),
  fetch("https://api.example.com/posts").then(r => r.json()),
  fetch("https://api.example.com/comments").then(r => r.json()),
]);

console.log("用户:", users.length);
console.log("文章:", posts.length);
console.log("评论:", comments.length);

Promise.allSettled

typescript
// 即使部分失败也继续
const results = await Promise.allSettled([
  fetch("https://api.example.com/users").then(r => r.json()),
  fetch("https://api.example.com/may-fail").then(r => r.json()),
  fetch("https://api.example.com/posts").then(r => r.json()),
]);

results.forEach((result, index) => {
  if (result.status === "fulfilled") {
    console.log(`请求 ${index} 成功:`, result.value);
  } else {
    console.log(`请求 ${index} 失败:`, result.reason);
  }
});

限制并发数

typescript
async function fetchWithConcurrency<T>(
  urls: string[],
  concurrency: number,
  fetcher: (url: string) => Promise<T>
): Promise<T[]> {
  const results: T[] = [];
  const executing: Promise<void>[] = [];

  for (const url of urls) {
    const promise = fetcher(url).then(result => {
      results.push(result);
    });

    executing.push(promise);

    if (executing.length >= concurrency) {
      await Promise.race(executing);
      executing.splice(
        executing.findIndex(p => p === promise),
        1
      );
    }
  }

  await Promise.all(executing);
  return results;
}

// 使用:最多同时 3 个请求
const urls = [
  "https://api.example.com/1",
  "https://api.example.com/2",
  "https://api.example.com/3",
  "https://api.example.com/4",
  "https://api.example.com/5",
];

const results = await fetchWithConcurrency(
  urls,
  3,
  url => fetch(url).then(r => r.json())
);

代理和 TLS

HTTP 代理

typescript
// 使用环境变量
process.env.HTTP_PROXY = "http://proxy.example.com:8080";
process.env.HTTPS_PROXY = "http://proxy.example.com:8080";

const response = await fetch("https://api.example.com");

自定义 TLS

typescript
const response = await fetch("https://internal-api.company.com", {
  tls: {
    // 自签名证书
    rejectUnauthorized: false,
    
    // 或指定 CA
    ca: Bun.file("./certs/ca.pem"),
  },
});

下载文件

typescript
// 下载并保存文件
async function downloadFile(url: string, savePath: string) {
  const response = await fetch(url);
  
  if (!response.ok) {
    throw new Error(`下载失败: ${response.status}`);
  }
  
  await Bun.write(savePath, response);
  console.log(`文件已保存到: ${savePath}`);
}

// 使用
await downloadFile(
  "https://example.com/large-file.zip",
  "./downloads/file.zip"
);

下载进度

typescript
async function downloadWithProgress(url: string, savePath: string) {
  const response = await fetch(url);
  const contentLength = response.headers.get("content-length");
  const total = contentLength ? parseInt(contentLength) : 0;
  
  let downloaded = 0;
  const reader = response.body?.getReader();
  const chunks: Uint8Array[] = [];
  
  if (reader) {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      chunks.push(value);
      downloaded += value.length;
      
      if (total) {
        const percent = ((downloaded / total) * 100).toFixed(1);
        console.log(`下载进度: ${percent}%`);
      }
    }
  }
  
  // 合并并保存
  const blob = new Blob(chunks);
  await Bun.write(savePath, blob);
}

小结

本章介绍了:

  • ✅ 基本 HTTP 请求方法
  • ✅ 请求配置和响应处理
  • ✅ 表单和文件上传
  • ✅ 错误处理和超时
  • ✅ 重试机制和并发控制
  • ✅ HTTP 客户端封装
  • ✅ 文件下载

下一步

继续阅读 打包构建 了解 Bun 的内置打包工具。

本站内容仅供学习和研究使用。