Fetch
使用 Bun 的 fetch API 发送 HTTP 请求
Bun 实现了 WHATWG 的 fetch 标准,同时进行了扩展以满足服务端 JavaScript 的需求。
Bun 也实现了 node:http,但我们通常建议使用 fetch。
发送 HTTP 请求
要发送 HTTP 请求,请使用 fetch:
const response = await fetch("http://example.com");
console.log(response.status); // => 200
const text = await response.text(); // 或者 response.json(), response.formData() 等等
fetch 也支持 HTTPS URL。
const response = await fetch("https://example.com");
你也可以传递一个 Request 对象给 fetch。
const request = new Request("http://example.com", {
method: "POST",
body: "Hello, world!",
});
const response = await fetch(request);
发送 POST 请求
要发送 POST 请求,传入一个 method 属性值为 "POST" 的对象。
const response = await fetch("http://example.com", {
method: "POST",
body: "Hello, world!",
});
body 可以是字符串、FormData 对象、ArrayBuffer、Blob 或 MDN 文档中列出的其他请求体类型。
代理请求
要代理请求,传入一个对象,并将 proxy 属性设置为 URL 字符串、URL 实例,或将其设置为一个 url 为字符串或 URL 的对象:
const response = await fetch("http://example.com", {
proxy: "http://proxy.com",
});
要向代理服务器发送自定义请求头,请传入一个对象:
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.com | example.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 则可强制直接连接,不受环境影响:
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。
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 属性的对象。
const response = await fetch("http://example.com", {
headers: {
"X-Custom-Header": "value",
},
});
你也可以使用 Headers 对象设置请求头。
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。
流式响应体
你可以使用异步迭代器来流式读取响应体。
const response = await fetch("http://example.com");
for await (const chunk of response.body) {
console.log(chunk);
}
你也可以直接访问 ReadableStream。
const response = await fetch("http://example.com");
const stream = response.body;
const reader = stream.getReader();
const { value, done } = await reader.read();
流式请求体
你也可以使用 ReadableStream 来流式传输请求体数据:
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:
const response = await fetch("http://example.com", {
signal: AbortSignal.timeout(1000),
});
取消请求
要取消请求,使用 AbortController:
const controller = new AbortController();
const response = await fetch("http://example.com", {
signal: controller.signal,
});
controller.abort();
Unix 域套接字
要通过 Unix 域套接字请求 URL,使用 unix: string 选项:
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 选项:
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 选项:
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:
await fetch("https://example.com", {
tls: {
rejectUnauthorized: false,
},
});
这可以避免使用自签名证书时出现 SSL 错误,但会禁用 TLS 验证,因此请谨慎使用。
请求选项
除了标准的 fetch 选项,Bun 还提供了几个扩展:
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 桶读取。
// 使用环境变量提供凭据
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: 协议获取本地文件:
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 会自动规范化路径:
// 以下两者都能在 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 方案:
const response = await fetch("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==");
const text = await response.text(); // "Hello, World!"
Blob URL - blob:
你可以使用 URL.createObjectURL() 创建的 URL 来获取 Blob:
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:
const response = await fetch("http://example.com", {
verbose: true,
});
这会将请求和响应标头打印到终端:
[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。
import { dns } from "bun";
dns.prefetch("bun.com");
DNS 缓存
默认情况下,Bun 会在内存中缓存并去重 DNS 查询,最长缓存 30 秒。dns.getCacheStats() 会返回缓存统计信息。
参见 DNS 缓存。
预连接到主机
fetch.preconnect 会在你准备向某个主机发送请求之前,启动 DNS 查询、TCP 套接字连接和 TLS 握手。
import { fetch } from "bun";
fetch.preconnect("https://bun.sh");
在 fetch.preconnect 后立即调用 fetch 并不会让请求更快。只有在确定主机和发送请求之间存在时间间隔时,预连接才能发挥作用。
启动时预连接
要在启动时预连接到某个主机,请传入 --fetch-preconnect:
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 之间共享连接。
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。 |
keepAlive | false 会在每个连接收到响应后将其关闭。idleTimeout 的单位为秒(默认值为 300)。maxIdleSockets 限制每种类型(普通连接、TLS、Unix 套接字)的空闲连接数。这两个限制均不适用于 HTTP/3 连接。 |
unix | Unix 套接字路径。 |
请求中给出的选项优先于 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 验证证书。
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 环境变量:
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 将响应体写入磁盘文件:
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 提供的专属扩展。