Bun

HTTP 服务器

此页面主要介绍 Bun 原生的 Bun.serve API。Bun 也实现了 fetch 和 Node.js 的 http 以及 https 模块。

这些模块已被重新实现,以使用 Bun 快速的内部 HTTP 基础设施。请随意直接使用这些模块;依赖于这些模块的框架(如 Express)应该可以开箱即用。有关细粒度的兼容性信息,请参阅运行时 > Node.js API

要启动具有简洁 API 的高性能 HTTP 服务器,推荐的方法是 Bun.serve

Bun.serve()

使用 Bun.serve 在 Bun 中启动 HTTP 服务器。

Bun.serve({
  // `routes` requires Bun v1.2.3+
  routes: {
    // Static routes
    "/api/status": new Response("OK"),

    // Dynamic routes
    "/users/:id": req => {
      return new Response(`Hello User ${req.params.id}!`);
    },

    // Per-HTTP method handlers
    "/api/posts": {
      GET: () => new Response("List posts"),
      POST: async req => {
        const body = await req.json();
        return Response.json({ created: true, ...body });
      },
    },

    // Wildcard route for all routes that start with "/api/" and aren't otherwise matched
    "/api/*": Response.json({ message: "Not found" }, { status: 404 }),

    // Redirect from /blog/hello to /blog/hello/world
    "/blog/hello": Response.redirect("/blog/hello/world"),

    // Serve a file by buffering it in memory
    "/favicon.ico": new Response(await Bun.file("./favicon.ico").bytes(), {
      headers: {
        "Content-Type": "image/x-icon",
      },
    }),
  },

  // (optional) fallback for unmatched routes:
  // Required if Bun's version < 1.2.3
  fetch(req) {
    return new Response("Not Found", { status: 404 });
  },
});

路由

Bun.serve() 中的路由接收 BunRequest(它扩展了 Request)并返回 ResponsePromise<Response>。这使得为发送和接收 HTTP 请求使用相同的代码更加容易。

// Simplified for brevity
interface BunRequest<T extends string> extends Request {
  params: Record<T, string>;
}

路由中的 Async/await

你可以在路由处理程序中使用 async/await 来返回 Promise<Response>

import { sql, serve } from "bun";

serve({
  port: 3001,
  routes: {
    "/api/version": async () => {
      const [version] = await sql`SELECT version()`;
      return Response.json(version);
    },
  },
});

路由中的 Promise

你也可以从路由处理程序返回 Promise<Response>

import { sql, serve } from "bun";

serve({
  routes: {
    "/api/version": () => {
      return new Promise(resolve => {
        setTimeout(async () => {
          const [version] = await sql`SELECT version()`;
          resolve(Response.json(version));
        }, 100);
      });
    },
  },
});

类型安全的路由参数

当以字符串字面量形式传递路由参数时,TypeScript 会解析路由参数,以便你的编辑器在访问 request.params 时显示自动完成。

import type { BunRequest } from "bun";

Bun.serve({
  routes: {
    // TypeScript knows the shape of params when passed as a string literal
    "/orgs/:orgId/repos/:repoId": req => {
      const { orgId, repoId } = req.params;
      return Response.json({ orgId, repoId });
    },

    "/orgs/:orgId/repos/:repoId/settings": (
      // optional: you can explicitly pass a type to BunRequest:
      req: BunRequest<"/orgs/:orgId/repos/:repoId/settings">,
    ) => {
      const { orgId, repoId } = req.params;
      return Response.json({ orgId, repoId });
    },
  },
});

百分比编码的路由参数值会自动解码。支持 Unicode 字符。无效的 Unicode 字符将被 Unicode 替换字符 &0xFFFD; 替换。

静态响应

路由也可以是 Response 对象(不带处理函数)。Bun.serve() 对其进行了优化,实现零分配调度 - 非常适合健康检查、重定向和固定内容

Bun.serve({
  routes: {
    // Health checks
    "/health": new Response("OK"),
    "/ready": new Response("Ready", {
      headers: {
        // Pass custom headers
        "X-Ready": "1",
      },
    }),

    // Redirects
    "/blog": Response.redirect("https://bun.net.cn/blog"),

    // API responses
    "/api/config": Response.json({
      version: "1.0.0",
      env: "production",
    }),
  },
});

静态响应在初始化后不会分配额外的内存。通常,你可以预期比手动返回 Response 对象至少提高 15% 的性能。

静态路由响应在服务器对象的生命周期内被缓存。要重新加载静态路由,请调用 server.reload(options)

const server = Bun.serve({
  static: {
    "/api/time": new Response(new Date().toISOString()),
  },

  fetch(req) {
    return new Response("404!");
  },
});

// Update the time every second.
setInterval(() => {
  server.reload({
    static: {
      "/api/time": new Response(new Date().toISOString()),
    },

    fetch(req) {
      return new Response("404!");
    },
  });
}, 1000);

重新加载路由只会影响下一个请求。正在处理的请求将继续使用旧路由。在完成对旧路由的正在处理的请求后,旧路由将从内存中释放。

为了简化错误处理,静态路由不支持从 ReadableStreamAsyncIterator 流式传输响应体。幸运的是,你仍然可以先将响应缓冲到内存中

const time = await fetch("https://api.example.com/v1/data");
// Buffer the response in memory first.
const blob = await time.blob();

const server = Bun.serve({
  static: {
    "/api/data": new Response(blob),
  },

  fetch(req) {
    return new Response("404!");
  },
});

路由优先级

路由按照以下优先级顺序匹配:

  1. 精确路由 (/users/all)
  2. 参数路由 (/users/:id)
  3. 通配符路由 (/users/*)
  4. 全局捕获所有路由 (/*)
Bun.serve({
  routes: {
    // Most specific first
    "/api/users/me": () => new Response("Current user"),
    "/api/users/:id": req => new Response(`User ${req.params.id}`),
    "/api/*": () => new Response("API catch-all"),
    "/*": () => new Response("Global catch-all"),
  },
});

每个 HTTP 方法的路由

路由处理程序可以按 HTTP 方法进行专门化

Bun.serve({
  routes: {
    "/api/posts": {
      // Different handlers per method
      GET: () => new Response("List posts"),
      POST: async req => {
        const post = await req.json();
        return Response.json({ id: crypto.randomUUID(), ...post });
      },
      PUT: async req => {
        const updates = await req.json();
        return Response.json({ updated: true, ...updates });
      },
      DELETE: () => new Response(null, { status: 204 }),
    },
  },
});

你可以传递以下任何方法:

方法用例示例
GET获取资源
HEAD检查资源是否存在
OPTIONS获取允许的 HTTP 方法 (CORS)
DELETE删除资源
PATCH更新资源
POST创建资源
PUT更新资源

当传递函数而不是对象时,所有方法都将由该函数处理

const server = Bun.serve({
  routes: {
    "/api/version": () => Response.json({ version: "1.0.0" }),
  },
});

await fetch(new URL("/api/version", server.url));
await fetch(new URL("/api/version", server.url), { method: "PUT" });
// ... etc

热路由重载

使用 server.reload() 更新路由,无需重启服务器

const server = Bun.serve({
  routes: {
    "/api/version": () => Response.json({ version: "1.0.0" }),
  },
});

// Deploy new routes without downtime
server.reload({
  routes: {
    "/api/version": () => Response.json({ version: "2.0.0" }),
  },
});

错误处理

Bun 为路由提供结构化错误处理

Bun.serve({
  routes: {
    // Errors are caught automatically
    "/api/risky": () => {
      throw new Error("Something went wrong");
    },
  },
  // Global error handler
  error(error) {
    console.error(error);
    return new Response(`Internal Error: ${error.message}`, {
      status: 500,
      headers: {
        "Content-Type": "text/plain",
      },
    });
  },
});

HTML 导入

要添加客户端单页应用程序,你可以使用 HTML 导入

import myReactSinglePageApp from "./index.html";

Bun.serve({
  routes: {
    "/": myReactSinglePageApp,
  },
});

HTML 导入不仅仅提供 HTML。它是一个功能齐全的前端打包器、转译器和工具包,使用 Bun 的 打包器、JavaScript 转译器和 CSS 解析器构建。

你可以使用它来构建功能齐全的前端,包括 React、TypeScript、Tailwind CSS 等。查看 /docs/bundler/fullstack 了解更多信息。

实际示例:REST API

这是一个使用 Bun 路由器和零依赖的基本数据库支持的 REST API

server.ts
types.ts
server.ts
import type { Post } from "./types.ts";
import { Database } from "bun:sqlite";

const db = new Database("posts.db");
db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    created_at TEXT NOT NULL
  )
`);

Bun.serve({
  routes: {
    // List posts
    "/api/posts": {
      GET: () => {
        const posts = db.query("SELECT * FROM posts").all();
        return Response.json(posts);
      },

      // Create post
      POST: async req => {
        const post: Omit<Post, "id" | "created_at"> = await req.json();
        const id = crypto.randomUUID();

        db.query(
          `INSERT INTO posts (id, title, content, created_at)
           VALUES (?, ?, ?, ?)`,
        ).run(id, post.title, post.content, new Date().toISOString());

        return Response.json({ id, ...post }, { status: 201 });
      },
    },

    // Get post by ID
    "/api/posts/:id": req => {
      const post = db
        .query("SELECT * FROM posts WHERE id = ?")
        .get(req.params.id);

      if (!post) {
        return new Response("Not Found", { status: 404 });
      }

      return Response.json(post);
    },
  },

  error(error) {
    console.error(error);
    return new Response("Internal Server Error", { status: 500 });
  },
});
types.ts
export interface Post {
  id: string;
  title: string;
  content: string;
  created_at: string;
}

路由性能

Bun.serve() 的路由器构建于 uWebSocket 的 基于树的方法之上,添加了 SIMD 加速的路由参数解码JavaScriptCore 结构缓存,以将现代硬件的性能发挥到极致。

fetch 请求处理程序

fetch 处理程序处理任何路由都未匹配的传入请求。它接收 Request 对象并返回 ResponsePromise<Response>

Bun.serve({
  fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/") return new Response("Home page!");
    if (url.pathname === "/blog") return new Response("Blog!");
    return new Response("404!");
  },
});

fetch 处理程序支持 async/await

import { sleep, serve } from "bun";
serve({
  async fetch(req) {
    const start = performance.now();
    await sleep(10);
    const end = performance.now();
    return new Response(`Slept for ${end - start}ms`);
  },
});

也支持基于 Promise 的响应

Bun.serve({
  fetch(req) {
    // Forward the request to another server.
    return fetch("https://example.com");
  },
});

你还可以从 fetch 处理程序访问 Server 对象。它是传递给 fetch 函数的第二个参数。

// `server` is passed in as the second argument to `fetch`.
const server = Bun.serve({
  fetch(req, server) {
    const ip = server.requestIP(req);
    return new Response(`Your IP is ${ip}`);
  },
});

更改 porthostname

要配置服务器将监听的端口和主机名,请在 options 对象中设置 porthostname

Bun.serve({
  port: 8080, // defaults to $BUN_PORT, $PORT, $NODE_PORT otherwise 3000
  hostname: "mydomain.com", // defaults to "0.0.0.0"
  fetch(req) {
    return new Response("404!");
  },
});

要随机选择一个可用端口,请将 port 设置为 0

const server = Bun.serve({
  port: 0, // random port
  fetch(req) {
    return new Response("404!");
  },
});

// server.port is the randomly selected port
console.log(server.port);

你可以通过访问服务器对象上的 port 属性或访问 url 属性来查看选择的端口。

console.log(server.port); // 3000
console.log(server.url); // http://localhost:3000

配置默认端口

Bun 支持多个选项和环境变量来配置默认端口。当未设置 port 选项时,将使用默认端口。

  • --port 命令行标志
bun --port=4002 server.ts
  • BUN_PORT 环境变量
BUN_PORT=4002 bun server.ts
  • PORT 环境变量
PORT=4002 bun server.ts
  • NODE_PORT 环境变量
NODE_PORT=4002 bun server.ts

Unix 域套接字

要在 unix 域套接字上监听,请传递带有套接字路径的 unix 选项。

Bun.serve({
  unix: "/tmp/my-socket.sock", // path to socket
  fetch(req) {
    return new Response(`404!`);
  },
});

抽象命名空间套接字

Bun 支持 Linux 抽象命名空间套接字。要使用抽象命名空间套接字,请在 unix 路径前加上空字节。

Bun.serve({
  unix: "\0my-abstract-socket", // abstract namespace socket
  fetch(req) {
    return new Response(`404!`);
  },
});

与 unix 域套接字不同,抽象命名空间套接字不绑定到文件系统,并且在对套接字的最后一个引用关闭时会自动删除。

错误处理

要激活开发模式,请设置 development: true

Bun.serve({
  development: true,
  fetch(req) {
    throw new Error("woops!");
  },
});

在开发模式下,Bun 将在浏览器中使用内置错误页面显示错误。

Bun 的内置 500 页面

error 回调

要处理服务器端错误,请实现 error 处理程序。此函数应返回 Response,以便在发生错误时提供给客户端。此响应将取代 development 模式下 Bun 的默认错误页面。

Bun.serve({
  fetch(req) {
    throw new Error("woops!");
  },
  error(error) {
    return new Response(`<pre>${error}\n${error.stack}</pre>`, {
      headers: {
        "Content-Type": "text/html",
      },
    });
  },
});

调用 Bun.serve 返回一个 Server 对象。要停止服务器,请调用 .stop() 方法。

const server = Bun.serve({
  fetch() {
    return new Response("Bun!");
  },
});

server.stop();

TLS

Bun 开箱即用地支持 TLS,由 BoringSSL 提供支持。通过传入 keycert 的值来启用 TLS;启用 TLS 需要同时提供这两个值。

Bun.serve({
  fetch(req) {
    return new Response("Hello!!!");
  },

  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
  }
});

keycert 字段期望你的 TLS 密钥和证书的内容而不是其路径。这可以是字符串、BunFileTypedArrayBuffer

Bun.serve({
  fetch() {},

  tls: {
    // BunFile
    key: Bun.file("./key.pem"),
    // Buffer
    key: fs.readFileSync("./key.pem"),
    // string
    key: fs.readFileSync("./key.pem", "utf8"),
    // array of above
    key: [Bun.file("./key1.pem"), Bun.file("./key2.pem")],
  },
});

如果你的私钥使用密码加密,请提供 passphrase 的值以解密它。

Bun.serve({
  fetch(req) {
    return new Response("Hello!!!");
  },

  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
    passphrase: "my-secret-passphrase",
  }
});

可选地,你可以通过传递 ca 的值来覆盖受信任的 CA 证书。默认情况下,服务器将信任 Mozilla 策划的知名 CA 列表。当指定 ca 时,Mozilla 列表将被覆盖。

Bun.serve({
  fetch(req) {
    return new Response("Hello!!!");
  },
  tls: {
    key: Bun.file("./key.pem"), // path to TLS key
    cert: Bun.file("./cert.pem"), // path to TLS cert
    ca: Bun.file("./ca.pem"), // path to root CA certificate
  }
});

要覆盖 Diffie-Hellman 参数

Bun.serve({
  // ...
  tls: {
    // other config
    dhParamsFile: "/path/to/dhparams.pem", // path to Diffie Hellman parameters
  },
});

服务器名称指示 (SNI)

要配置服务器的服务器名称指示 (SNI),请在 tls 对象中设置 serverName 字段。

Bun.serve({
  // ...
  tls: {
    // ... other config
    serverName: "my-server.com", // SNI
  },
});

要允许多个服务器名称,请将对象数组传递给 tls,每个对象都包含一个 serverName 字段。

Bun.serve({
  // ...
  tls: [
    {
      key: Bun.file("./key1.pem"),
      cert: Bun.file("./cert1.pem"),
      serverName: "my-server1.com",
    },
    {
      key: Bun.file("./key2.pem"),
      cert: Bun.file("./cert2.pem"),
      serverName: "my-server2.com",
    },
  ],
});

idleTimeout

要配置空闲超时,请在 Bun.serve 中设置 idleTimeout 字段。

Bun.serve({
  // 10 seconds:
  idleTimeout: 10,

  fetch(req) {
    return new Response("Bun!");
  },
});

这是在服务器关闭连接之前,允许连接空闲的最长时间。如果未发送或接收任何数据,则连接处于空闲状态。

export default 语法

到目前为止,此页面上的示例都使用了显式的 Bun.serve API。Bun 也支持一种替代语法。

server.ts
import {type Serve} from "bun";

export default {
  fetch(req) {
    return new Response("Bun!");
  },
} satisfies Serve;

与其将服务器选项传递到 Bun.serve 中,不如 export default 导出它。此文件可以直接执行;当 Bun 看到一个包含 default 导出并且其中包含 fetch 处理程序的文件时,它会在底层将其传递到 Bun.serve 中。

流式传输文件

要流式传输文件,请返回一个以 BunFile 对象作为主体的 Response 对象。

Bun.serve({
  fetch(req) {
    return new Response(Bun.file("./hello.txt"));
  },
});

⚡️ 速度 — Bun 在可能的情况下自动使用 sendfile(2) 系统调用,从而在内核中实现零拷贝文件传输——这是发送文件的最快方式。

你可以使用 Bun.file 对象上的 slice(start, end) 方法发送文件的一部分。这会自动在 Response 对象上设置 Content-RangeContent-Length 标头。

Bun.serve({
  fetch(req) {
    // parse `Range` header
    const [start = 0, end = Infinity] = req.headers
      .get("Range") // Range: bytes=0-100
      .split("=") // ["Range: bytes", "0-100"]
      .at(-1) // "0-100"
      .split("-") // ["0", "100"]
      .map(Number); // [0, 100]

    // return a slice of the file
    const bigFile = Bun.file("./big-video.mp4");
    return new Response(bigFile.slice(start, end));
  },
});

服务器生命周期方法

server.stop() - 停止服务器

要停止服务器接受新连接

const server = Bun.serve({
  fetch(req) {
    return new Response("Hello!");
  },
});

// Gracefully stop the server (waits for in-flight requests)
await server.stop();

// Force stop and close all active connections
await server.stop(true);

默认情况下,stop() 允许正在处理的请求和 WebSocket 连接完成。传递 true 以立即终止所有连接。

server.ref() 和 server.unref() - 进程生命周期控制

控制服务器是否保持 Bun 进程存活

// Don't keep process alive if server is the only thing running
server.unref();

// Restore default behavior - keep process alive
server.ref();

server.reload() - 热重载处理程序

更新服务器的处理程序,无需重启

const server = Bun.serve({
  routes: {
    "/api/version": Response.json({ version: "v1" }),
  },
  fetch(req) {
    return new Response("v1");
  },
});

// Update to new handler
server.reload({
  routes: {
    "/api/version": Response.json({ version: "v2" }),
  },
  fetch(req) {
    return new Response("v2");
  },
});

这对于开发和热重载非常有用。只有 fetcherrorroutes 可以被更新。

每个请求的控制

server.timeout(Request, seconds) - 自定义请求超时

为单个请求设置自定义空闲超时

const server = Bun.serve({
  fetch(req, server) {
    // Set 60 second timeout for this request
    server.timeout(req, 60);

    // If they take longer than 60 seconds to send the body, the request will be aborted
    await req.text();

    return new Response("Done!");
  },
});

传递 0 以禁用请求的超时。

server.requestIP(Request) - 获取客户端信息

获取客户端 IP 和端口信息

const server = Bun.serve({
  fetch(req, server) {
    const address = server.requestIP(req);
    if (address) {
      return new Response(
        `Client IP: ${address.address}, Port: ${address.port}`,
      );
    }
    return new Response("Unknown client");
  },
});

对于已关闭的请求或 Unix 域套接字,返回 null

服务器指标

server.pendingRequests 和 server.pendingWebSockets

使用内置计数器监控服务器活动

const server = Bun.serve({
  fetch(req, server) {
    return new Response(
      `Active requests: ${server.pendingRequests}\n` +
        `Active WebSockets: ${server.pendingWebSockets}`,
    );
  },
});

server.subscriberCount(topic) - WebSocket 订阅者

获取 WebSocket 主题的订阅者数量

const server = Bun.serve({
  fetch(req, server) {
    const chatUsers = server.subscriberCount("chat");
    return new Response(`${chatUsers} users in chat`);
  },
  websocket: {
    message(ws) {
      ws.subscribe("chat");
    },
  },
});

WebSocket 配置

server.publish(topic, data, compress) - WebSocket 消息发布

服务器可以向所有订阅了某个 WebSocket 主题的客户端发布消息

const server = Bun.serve({
  websocket: {
    message(ws) {
      // Publish to all "chat" subscribers
      server.publish("chat", "Hello everyone!");
    },
  },

  fetch(req) {
    // ...
  },
});

publish() 方法返回

  • 如果成功,则返回已发送的字节数
  • 如果消息被丢弃,则返回 0
  • 如果应用了背压,则返回 -1

WebSocket 处理程序选项

配置 WebSocket 时,可以通过 websocket 处理程序使用几个高级选项

Bun.serve({
  websocket: {
    // Maximum message size (in bytes)
    maxPayloadLength: 64 * 1024,

    // Backpressure limit before messages are dropped
    backpressureLimit: 1024 * 1024,

    // Close connection if backpressure limit is hit
    closeOnBackpressureLimit: true,

    // Handler called when backpressure is relieved
    drain(ws) {
      console.log("Backpressure relieved");
    },

    // Enable per-message deflate compression
    perMessageDeflate: {
      compress: true,
      decompress: true,
    },

    // Send ping frames to keep connection alive
    sendPings: true,

    // Handlers for ping/pong frames
    ping(ws, data) {
      console.log("Received ping");
    },
    pong(ws, data) {
      console.log("Received pong");
    },

    // Whether server receives its own published messages
    publishToSelf: false,
  },
});

基准测试

以下是 Bun 和 Node.js 实现的简单 HTTP 服务器,该服务器对每个传入的 Request 响应 Bun!

Bun
Node
Bun
Bun.serve({
  fetch(req: Request) {
    return new Response("Bun!");
  },
  port: 3000,
});
Node
require("http")
  .createServer((req, res) => res.end("Bun!"))
  .listen(8080);

在 Linux 上,Bun.serve 服务器每秒可以处理大约 2.5 倍于 Node.js 的请求。

运行时每秒请求数
Node 16~64,000
Bun~160,000
image

参考

请参阅 TypeScript 定义