运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

Fetch

使用 Bun 的 fetch API 发送 HTTP 请求

Bun 实现了 WHATWG 的 fetch 标准,同时进行了扩展以满足服务端 JavaScript 的需求。

Bun 也实现了 node:http,但我们通常建议使用 fetch。

发送 HTTP 请求

要发送 HTTP 请求,请使用 fetch:

TypeScript
const response = await fetch("http://example.com");

console.log(response.status); // => 200

const text = await response.text(); // 或者 response.json(), response.formData() 等等

fetch 也支持 HTTPS URL。

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

你也可以传递一个 Request 对象给 fetch。

TypeScript
const request = new Request("http://example.com", {
  method: "POST",
  body: "Hello, world!",
});

const response = await fetch(request);

发送 POST 请求

要发送 POST 请求,传入一个 method 属性值为 "POST" 的对象。

TypeScript
const response = await fetch("http://example.com", {
  method: "POST",
  body: "Hello, world!",
});

body 可以是字符串、FormData 对象、ArrayBuffer、Blob 或 MDN 文档中列出的其他请求体类型。

代理请求

要代理请求,传入一个对象,并将 proxy 属性设置为 URL 字符串、URL 实例,或将其设置为一个 url 为字符串或 URL 的对象:

TypeScript
const response = await fetch("http://example.com", {
  proxy: "http://proxy.com",
});

要向代理服务器发送自定义请求头,请传入一个对象:

TypeScript
const response = await fetch("http://example.com", {
  proxy: {
    url: "http://proxy.com",
    headers: {
      "Proxy-Authorization": "Bearer my-token",
      "X-Custom-Proxy-Header": "value",
    },
  },
});

对于 HTTPS 目标,Bun 会在 CONNECT 请求中将 headers 直接发送给代理;对于 HTTP 目标,则会在代理请求中发送。如果你提供了 Proxy-Authorization 请求头,它会覆盖代理 URL 中的任何凭据。

如果没有 proxy 选项,Bun 会在每次运行 fetch 时读取 HTTP_PROXY、HTTPS_PROXY 和 ALL_PROXY(大写或小写),因此对 process.env 的更改会应用于下一个请求。如果未设置特定于协议的变量,Bun 会对 http: 和 https: URL 一律使用 ALL_PROXY。如果值指定了其他类型的代理(例如 socks5://),Bun 会忽略它。curl 也会读取 ALL_PROXY;Node.js 则不会。带有 unix 选项的请求永远不会使用代理环境变量。

no_proxy 列出绕过代理的主机。与 curl 和 Node.js 一样,当 no_proxy 未设置或为空时,Bun 会读取 NO_PROXY。条目之间以逗号或空格分隔。

条目匹配对象
*所有主机
example.com、.example.com、*.example.comexample.com 及其子域名
127.0.0.1、::1、[::1]该 IP 地址
10.0.0.0/8、fd00::/8该地址块中的所有 IP 地址
example.com:8080仅该主机的该端口

NO_PROXY 也适用于显式指定的 proxy。设置 respectNoProxy: false 可强制通过代理发送请求;设置 proxy: false 则可强制直接连接,不受环境影响:

TypeScript
await fetch("https://example.com", {
  proxy: { url: "http://proxy.com", respectNoProxy: false },
});

await fetch("https://example.com", { proxy: false });

当代理对 CONNECT 的响应状态码不是 2xx 时,fetch 会以 ERR_PROXY_TUNNEL 错误拒绝。该错误包含代理的 status、statusText 和 headers。

TypeScript
try {
  await fetch("https://example.com", { proxy: "http://proxy.com" });
} catch (error) {
  if (error.code === "ERR_PROXY_TUNNEL") {
    console.log(error.status, error.headers.get("proxy-authenticate"));
  }
}

自定义请求头

要设置自定义请求头,传入一个带有 headers 属性的对象。

TypeScript
const response = await fetch("http://example.com", {
  headers: {
    "X-Custom-Header": "value",
  },
});

你也可以使用 Headers 对象设置请求头。

TypeScript
const headers = new Headers();
headers.append("X-Custom-Header", "value");

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

除非请求本身设置了 Authorization 请求头,否则 Bun 会将 URL 中的凭据(https://user:password@example.com/)作为 Basic Authorization 请求头发送。Node.js 的 fetch 则会拒绝此类 URL。连接错误的 path 不会包含凭据。

响应体

要读取响应体,使用以下方法之一:

  • response.text(): Promise<string>:返回包含响应体文本的 Promise。
  • response.json(): Promise<any>:返回包含响应体 JSON 对象的 Promise。
  • response.formData(): Promise<FormData>:返回包含响应体 FormData 对象的 Promise。
  • response.bytes(): Promise<Uint8Array>:返回包含响应体 Uint8Array 的 Promise。
  • response.arrayBuffer(): Promise<ArrayBuffer>:返回包含响应体 ArrayBuffer 的 Promise。
  • response.blob(): Promise<Blob>:返回包含响应体 Blob 的 Promise。

流式响应体

你可以使用异步迭代器来流式读取响应体。

TypeScript
const response = await fetch("http://example.com");

for await (const chunk of response.body) {
  console.log(chunk);
}

你也可以直接访问 ReadableStream。

TypeScript
const response = await fetch("http://example.com");

const stream = response.body;

const reader = stream.getReader();
const { value, done } = await reader.read();

流式请求体

你也可以使用 ReadableStream 来流式传输请求体数据:

TypeScript
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("Hello");
    controller.enqueue(" ");
    controller.enqueue("World");
    controller.close();
  },
});

const response = await fetch("http://example.com", {
  method: "POST",
  body: stream,
});

使用 HTTP(S) 协议时的流式传输要点:

  • Bun 会将数据直接流式传输到网络,不会将整个请求体缓冲到内存中
  • 如果连接断开,Bun 会取消该流
  • 只有当流的大小已知时,Bun 才会自动设置 Content-Length 请求头

使用 S3 时:

  • 对于 PUT/POST 请求,Bun 会自动使用分段上传
  • Bun 会分块读取该流,并行上传各个数据块
  • 你可以通过 S3 选项监控进度

带超时的 URL 请求

要带超时发送请求,使用 AbortSignal.timeout:

TypeScript
const response = await fetch("http://example.com", {
  signal: AbortSignal.timeout(1000),
});

取消请求

要取消请求,使用 AbortController:

TypeScript
const controller = new AbortController();

const response = await fetch("http://example.com", {
  signal: controller.signal,
});

controller.abort();

Unix 域套接字

要通过 Unix 域套接字请求 URL,使用 unix: string 选项:

TypeScript
const response = await fetch("https://hostname/a/path", {
  unix: "/var/run/path/to/unix.sock",
  method: "POST",
  body: JSON.stringify({ message: "Hello from Bun!" }),
  headers: {
    "Content-Type": "application/json",
  },
});

TLS

使用客户端证书时,使用 tls 选项:

TypeScript
await fetch("https://example.com", {
  tls: {
    key: Bun.file("/path/to/key.pem"),
    cert: Bun.file("/path/to/cert.pem"),
    // ca: [Bun.file("/path/to/ca.pem")],
  },
});

自定义 TLS 验证

要自定义 TLS 验证,请在 tls 中使用 checkServerIdentity 选项:

TypeScript
await fetch("https://example.com", {
  tls: {
    checkServerIdentity: (hostname, peerCertificate) => {
      // 如果证书无效,返回一个 Error
    },
  },
});

此选项类似于 Node 的 tls 模块中的选项。Bun 会在验证证书链之后调用它。即使设置了 rejectUnauthorized: false,Bun 仍会调用它,并忽略其返回值。

该函数会取代 Bun 自己的主机名检查。返回 undefined 表示接受证书。要拒绝证书,请返回一个 Error 或抛出一个错误:fetch() promise 会以该错误拒绝。与 Node 一样,任何其他真值返回值也会导致请求失败。对象会原样作为拒绝原因。返回 Promise 或 true 之类的基本类型会导致 ERR_INVALID_RETURN_VALUE 错误,因此异步函数不会接受任何证书。与 Node 一样,所有假值返回值都会接受证书,因此 false 不会拒绝证书。出于这个原因,TypeScript 类型只允许 undefined 和 Error。

checkServerIdentity 必须是一个函数。null 和 undefined 表示没有函数。对于任何其他值,fetch() 都会以 ERR_INVALID_ARG_TYPE 错误拒绝。

通过自身 checkServerIdentity 检查的请求会打开自己的连接,并在之后关闭连接,因此其他请求的函数永远不会替它背书。若要每个连接只验证一次并复用该连接,请将函数设置在 Bun.FetchSession 上。

禁用 TLS 验证

要禁用 TLS 验证,将 rejectUnauthorized 设为 false:

TypeScript
await fetch("https://example.com", {
  tls: {
    rejectUnauthorized: false,
  },
});

这可以避免使用自签名证书时出现 SSL 错误,但会禁用 TLS 验证,因此请谨慎使用。

请求选项

除了标准的 fetch 选项,Bun 还提供了几个扩展:

TypeScript
const response = await fetch("http://example.com", {
  // 控制自动响应解压(默认为 true)
  // 支持 gzip、deflate、brotli (br) 和 zstd
  decompress: true,

  // 禁用此请求的连接复用
  keepalive: false,

  // 调试日志等级
  verbose: true, // 或 "curl" 以获得更详细的输出
});

协议支持

除了 HTTP(S) 之外,Bun 的 fetch 还支持多种其他协议:

S3 URL - s3://

Bun 支持直接从 S3 桶读取。

TypeScript
// 使用环境变量提供凭据
const response = await fetch("s3://my-bucket/path/to/object");

// 或者显式提供凭据
const response = await fetch("s3://my-bucket/path/to/object", {
  s3: {
    accessKeyId: "YOUR_ACCESS_KEY",
    secretAccessKey: "YOUR_SECRET_KEY",
    region: "us-east-1",
  },
});

使用 S3 时,只有 PUT 和 POST 方法支持请求体。对于上传,Bun 会自动对流式请求体使用分块上传。

请参阅 S3 文档。

本地文件 URL - file://

你可以用 file: 协议获取本地文件:

TypeScript
const response = await fetch("file:///path/to/file.txt");
const text = await response.text();

URL 的主机必须为空或为 localhost。fetch("file://other.host/path") 会以 ERR_INVALID_FILE_URL_HOST 错误拒绝。

在 Windows 上,Bun 会自动规范化路径:

TypeScript
// 以下两者都能在 Windows 上工作
const response = await fetch("file:///C:/path/to/file.txt");
const response2 = await fetch("file:///c:/path\\to/file.txt");

Data URL - data:

Bun 支持 data: URL 方案:

TypeScript
const response = await fetch("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==");
const text = await response.text(); // "Hello, World!"

Blob URL - blob:

你可以使用 URL.createObjectURL() 创建的 URL 来获取 Blob:

TypeScript
const blob = new Blob(["Hello, World!"], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const response = await fetch(url);

错误处理

Bun 的 fetch 实现包含多个特定错误场景:

  • 对 GET/HEAD 方法使用请求体会抛出错误(这符合 fetch API 的预期行为)
  • 同时使用 proxy 和 unix 选项会抛出错误
  • 连接失败时会以 TypeError 拒绝,且其 code 为套接字错误(ECONNREFUSED、ECONNRESET、ETIMEDOUT、ENOTFOUND 等)。消息以该错误代码开头。
  • 当 rejectUnauthorized 为 true(或未定义)时,TLS 证书验证失败
  • S3 操作可能会抛出与身份验证或权限相关的特定错误

Content-Type 处理

当未明确提供时,Bun 会自动为请求体设置 Content-Type 头:

  • 对于 Blob 对象,使用 Blob 的 type
  • 对于 FormData,设置合适的 multipart 边界

调试

要进行调试,请将 verbose: true 传递给 fetch:

TypeScript
const response = await fetch("http://example.com", {
  verbose: true,
});

这会将请求和响应标头打印到终端:

shell
[fetch] > HTTP/1.1 GET http://example.com/
[fetch] > Connection: keep-alive
[fetch] > User-Agent: Bun/1.3.3
[fetch] > Accept: */*
[fetch] > Host: example.com
[fetch] > Accept-Encoding: gzip, deflate, br, zstd

[fetch] < 200 OK
[fetch] < Content-Encoding: gzip
[fetch] < Age: 201555
[fetch] < Cache-Control: max-age=604800
[fetch] < Content-Type: text/html; charset=UTF-8
[fetch] < Date: Sun, 21 Jul 2024 02:41:14 GMT
[fetch] < Etag: "3147526947+gzip"
[fetch] < Expires: Sun, 28 Jul 2024 02:41:14 GMT
[fetch] < Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
[fetch] < Server: ECAcc (sac/254F)
[fetch] < Vary: Accept-Encoding
[fetch] < X-Cache: HIT
[fetch] < Content-Length: 648

verbose: boolean 是 Bun 特有的扩展,不属于 Web 标准的 fetch API。

性能

在发送 HTTP 请求之前,Bun 必须解析 DNS、连接 TCP 套接字,有时还要完成 TLS 握手。每个步骤都需要时间,尤其是在 DNS 服务器速度较慢或网络连接质量较差时。请求完成后,读取响应体同样需要时间和内存。

Bun 提供了用于优化这些步骤的 API。

DNS 预读取

当你知道即将连接某个主机,并希望避免初始 DNS 查询时,可以使用 dns.prefetch。

TypeScript
import { dns } from "bun";

dns.prefetch("bun.com");

DNS 缓存

默认情况下,Bun 会在内存中缓存并去重 DNS 查询,最长缓存 30 秒。dns.getCacheStats() 会返回缓存统计信息。

参见 DNS 缓存。

预连接到主机

fetch.preconnect 会在你准备向某个主机发送请求之前,启动 DNS 查询、TCP 套接字连接和 TLS 握手。

TypeScript
import { fetch } from "bun";

fetch.preconnect("https://bun.sh");

在 fetch.preconnect 后立即调用 fetch 并不会让请求更快。只有在确定主机和发送请求之间存在时间间隔时,预连接才能发挥作用。

启动时预连接

要在启动时预连接到某个主机,请传入 --fetch-preconnect:

shell
bun --fetch-preconnect https://bun.sh ./my-script.ts

--fetch-preconnect 类似于 HTML 中的 <link rel="preconnect">。Windows 尚未实现此功能;如果你需要在 Windows 上使用,请提交 issue。

连接池与 HTTP 长连接

Bun 会自动复用连接到同一主机的连接。这称为连接池,可以显著减少建立连接所需的时间。

Fetch sessions

Bun.FetchSession 保存连接设置并拥有一个 Keep-Alive 连接池。session.fetch 是使用该 session 的 fetch,且已绑定,因此可用于任何接受 fetch 函数的位置(new SomeClient({ fetch: session.fetch }))。它没有 preconnect。fetch(url, { session }) 通过全局 fetch 实现相同功能。Bun 绝不会在两个 session 之间,或 session 与普通 fetch 之间共享连接。

TypeScript
const session = new Bun.FetchSession({
  tls: { ca: await Bun.file("corp-ca.pem").text() },
  proxy: { url: "http://proxy.internal:8080", respectNoProxy: false },
  keepAlive: { idleTimeout: 30, maxIdleSockets: 8 },
});

const response = await session.fetch("https://example.com");
session.close(); // closes the idle connections
选项描述
tls与 fetch 的 tls 相同的选项。checkServerIdentity 每个连接运行一次。
proxy与 fetch 的 proxy 相同的值,包括 false。
keepAlivefalse 会在每个连接收到响应后将其关闭。idleTimeout 的单位为秒(默认值为 300)。maxIdleSockets 限制每种类型(普通连接、TLS、Unix 套接字)的空闲连接数。这两个限制均不适用于 HTTP/3 连接。
unixUnix 套接字路径。

请求中给出的选项优先于 session 的选项,包括 proxy: false。请求的 tls 会整体替换 session 的 tls。

如果构造函数遇到无法使用的 proxy 或 tls.checkServerIdentity,例如 proxy: 8080、缺少 url 的 proxy 对象,或不是函数的 checkServerIdentity,就会抛出错误。对于 proxy,只有 undefined、null 和 "" 表示未设置选项。在请求中,fetch 会忽略此类 proxy(true 会抛出错误),并保留其 session 或环境中的代理设置。

Bun 会将 idleTimeout 向上取整到套接字计时器的间隔;该间隔在四分钟内以 4 秒为步长递增,之后以整分钟为步长递增。连接绝不会在 idleTimeout 之前关闭,且最多可能比该时间多保持两个步长。idleTimeout 最长为 238 分钟。更大的值会按 238 分钟处理。

如果没有设置 maxIdleSockets,session 自身没有连接数限制。所有 session 和普通 fetch 的空闲连接会按类型共享一个有上限的连接池。连接池已满时,Bun 会关闭其中空闲时间最长的连接,无论它属于哪个 session。高于连接池大小的 maxIdleSockets 不会产生任何效果。

连接到特定地址

要将发往某个主机名的请求发送到你自行解析并检查过的地址,请将该地址放入 URL,并在 Host 请求头和 tls.serverName 中指定源站名称。Bun 会连接该地址,发送 Host(在 HTTP/2 中作为 :authority),使用 serverName 进行 SNI,并根据 serverName 验证证书。

TypeScript
import { lookup } from "node:dns/promises";

const { address, family } = await lookup("example.com");
if (isPrivateAddress(address)) throw new Error(`refusing to connect to ${address}`);

const response = await fetch(`https://${family === 6 ? `[${address}]` : address}/webhook`, {
  headers: { Host: "example.com" },
  tls: { serverName: "example.com" },
  proxy: false,
  redirect: "manual",
});
  • 传入 proxy: false。否则,Bun 会根据 URL 中的地址而非主机名判断环境代理和 NO_PROXY。请在解析主机名之前决定是否需要代理。
  • 自行跟随重定向(redirect: "manual"):Host 和 serverName 仅描述此源站,相对 Location 会基于该地址解析。
  • 如果一个名称对应多个地址,此处不会回退尝试其他地址。请自行逐个尝试。
  • Bun 会按原样发送 Host。如果端口不是该协议的默认端口,请包含端口。普通的 http:// URL 只需要 Host。
  • tls.serverName 适用于 HTTP/1.1 和 HTTP/2。protocol: "http3" 不接受该选项。
  • 每个不同的 serverName 都有自己的 TLS 上下文和 Keep-Alive 连接池。Bun 最多缓存 60 个。

同时连接数限制

默认情况下,Bun 将同时进行的 fetch 请求数限制为 256,原因有二:

  • 改善系统整体稳定性。操作系统对同时打开的 TCP 套接字数有上限,通常在几千左右。接近该限制时,会导致整台电脑异常,应用挂起或崩溃。
  • 鼓励 HTTP Keep-Alive 的连接复用。对于短时 HTTP 请求,最慢的步骤往往是初次连接。连接复用可节省大量时间。

超过限制时,Bun 会将请求排入队列,并在下一个请求结束后立即发送。

要提高该限制,请设置 BUN_CONFIG_MAX_HTTP_REQUESTS 环境变量:

shell
BUN_CONFIG_MAX_HTTP_REQUESTS=512 bun ./my-script.ts

该限制的最大值为 65,535。最大端口号也是 65,535,因此单台计算机很难超过这一限制。

响应体缓冲

读取响应体最快的方式是使用以下方法之一:

  • response.text(): Promise<string>
  • response.json(): Promise<any>
  • response.formData(): Promise<FormData>
  • response.bytes(): Promise<Uint8Array>
  • response.arrayBuffer(): Promise<ArrayBuffer>
  • response.blob(): Promise<Blob>

你也可以使用 Bun.write 将响应体写入磁盘文件:

TypeScript
import { write } from "bun";

await write("output.txt", response);

实现细节

  • 默认启用连接池。你可以通过 keepalive: false 或 "Connection: close" 请求头针对单个请求禁用连接池。
  • 在特定条件下,Bun 使用操作系统的 sendfile 系统调用优化大文件上传:
    • 文件必须大于 32KB
    • 请求不能使用代理或 compress 选项
    • 在 macOS 上,只有常规文件(非管道、套接字或设备)可以使用 sendfile
    • 不满足这些条件,或使用 S3/流式上传时,Bun 会改为将文件读入内存
    • Bun 仅对 HTTP(非 HTTPS)请求使用此优化,此时文件可直接从内核发送到网络堆栈;Bun 在 Windows 上不使用此优化
  • S3 操作会自动处理请求签名并合并身份验证请求头

其中许多功能是 Bun 针对标准 fetch API 提供的专属扩展。