HTTP
本章覆盖 ZebraRequest(请求对象)、请求体解析、响应 helpers、结构化错误(RFC 9457 Problem+Json)、静态文件与请求超时。
ZebraRequest
路由 handler 与中间件拿到的 req 是一个 ZebraRequest,包着 Web Standard Request:
interface ZebraRequest<P, B, Q> {
raw: Request; // 原始 Request
params: P; // 路径参数(路由字面量推断类型)
query: Q; // 查询参数(Record<string, string>)
headers: Headers;
url: URL;
body(): Promise<B>; // 按 content-type 解析的 body
json(): Promise<unknown>;
text(): Promise<string>;
form(): Promise<FormData>;
stream(): ReadableStream<Uint8Array>;
ctx: Map<symbol, unknown>; // 中间件共享数据的请求级 Map
ip?: string; // socket 对端地址(Bun requestIP)
signal: AbortSignal; // 取消信号(超时/客户端断开)
}query来自url.searchParams,重复键取最后一个。req.ctx是请求级共享状态(中间件写、handler 读),见 中间件。req.ip来自Bun.serve的server.requestIP(req),永远不从 header 推导;没有 Bun server(如app.dispatch()测试)时为undefined。x-forwarded-for只有在显式配置trustProxy时才被读取(由中间件如@zebra-web/rate-limit处理)。
请求体
惰性缓冲与流式读取
请求体是惰性解析 + 记忆化的:第一次调用 body() / json() / text() / form() 时缓冲一次字节,之后共享;stream() 不缓冲。
缓冲型 helper 可以混用或并发调用:例如 text() 与 json() 读取同一份字节, 各自应用解析规则;缓冲时按 content-type 执行大小限制。stream() 是互斥的流式读取方式,应在调用任何 缓冲型 helper 前选择。混用流式与缓冲读取,或重复请求原始流,都会抛出 TypeError(缓冲型 helper 返回 rejected promise)。
解析规则
| 方法 | 行为 |
|---|---|
body() | 按 content-type 解析:application/json → JSON;multipart/form-data → FormData(带 File 条目);application/x-www-form-urlencoded → 普通对象(重复键保留最后一个值);其他 → Uint8Array |
json() | 无视 content-type 强制按 JSON 解析。空 body → null;非法 JSON → 400 invalid_json |
text() | 原始文本 |
form() | multipart → FormData(File 条目,受 maxFiles/maxFileSize 约束);urlencoded → 字符串条目;其他 content-type → 空 FormData |
stream() | 原始流,经过同一个大小限制管道(limitStream)——大文件上传的不缓冲路径;限制触发时错误在流被读取时浮现 |
大小限制
构造时可覆盖(ZebraOptions.body),默认值:
{
maxSize: 1024 * 1024, // 1MB —— body()/json()/text()/form() 的通用上限
json: { limit: 1024 * 1024 }, // 1MB
form: { limit: 1024 * 1024 }, // 1MB
multipart: { limit: 16 * 1024 * 1024, maxFiles: 10, maxFileSize: 8 * 1024 * 1024 },
}Bun.serve 层的 maxRequestBodySize(ListenOptions,默认 128MB)是独立的传输层上限,先于任何 handler 执行。
所有应用层 body 上限(maxSize、json.limit、form.limit、multipart.limit、 multipart.maxFiles 和 multipart.maxFileSize)必须是有限数值。NaN 和正负无穷 会在构造 Zebra 时抛出 RangeError,错误信息包含字段名。零上限和小数上限仍受支持, 不会对上限取整。
const z = new Zebra({
body: { json: { limit: 256 * 1024 }, multipart: { maxFiles: 4 } },
});响应 helpers
来自 @zebra-web/core(@zebra-web/zebra 门面同样导出)。默认 content-type / 状态:
| Helper | 默认 content-type | 默认状态 |
|---|---|---|
json(value) | application/json; charset=utf-8 | 200 |
text(value) | text/plain; charset=utf-8 | 200 |
html(value) | text/html; charset=utf-8 | 200 |
stream(body) | application/octet-stream | 200 |
redirect(url) | —(无 body) | 302 |
import { html, json, redirect, stream, text } from "@zebra-web/zebra";
z.get("/api", () => json({ ok: true }));
z.get("/plain", () => text("hello")); // 无引号的原文
z.get("/page", () => html("<h1>Hi</h1>"));
z.get("/dl", () => stream(Bun.file("./x.bin")));
z.get("/old", () => redirect("/new")); // 或 { status: 301 }规则:
init.headers(任何形式:record / 数组 /Headers)里的content-type总是覆盖默认值。redirect的Location永远来自url参数;状态默认 302,可用init.status覆盖(如 301)。stream接受ReadableStream(SSE、分块)、Blob(Bun.file()即BunFile)、ArrayBuffer、类型化数组。
HttpError 与 Problem+Json
抛出结构化错误
import { HttpError } from "@zebra-web/zebra";
throw new HttpError(404, "board_not_found", "No such board");
throw new HttpError(429, "rate_limit_exceeded", "Too Many Requests", { limit: 10 }, {
"retry-after": "60",
});class HttpError extends Error {
constructor(
status: number, // 400–599(其他值抛 RangeError)
code: string, // 机器可读错误码
title: string, // 人类可读标题
detail?: unknown, // 附加详情(自动 JSON 安全序列化)
headers?: Record<string, string>, // 复制到响应头
);
}内置错误中间件把它转成:
{
"type": "https://errors.zebra.dev/rate_limit_exceeded",
"status": 429,
"title": "Too Many Requests",
"detail": { "limit": 10 },
"instance": "/api/posts"
}err.headers 原样进入响应头(HttpError 之外的内置错误码见下方「错误一览」)。
ValidationError
@zebra-web/core 的 ValidationError 携带 ValidationIssue[](path + message),渲染为 422 Problem+Json,errors 数组列出每个字段。契约实现(app.implement)的入参校验失败就是这个形状。
错误一览(内置 code)
| code | 状态 | 触发 |
|---|---|---|
not_found | 404 | 路径无匹配 |
method_not_allowed | 405 | 路径存在、方法不匹配(带 Allow 头) |
invalid_json | 400 | json() 遇到非法 JSON |
validation_failed | 422 | ValidationError |
request_timeout | 504 | requestTimeout 到期(detail.limit = 超时毫秒数) |
invalid_contract_response | 500 | 契约声明 204 但 handler 返回了 Response |
output_validation_failed | 500 | 契约输出校验失败 |
internal | 500 | 未识别错误 |
exposeStack: true 时,未知错误(非 HttpError/ValidationError)的 body 会带 stack。
静态文件
app.static(routePath, root, opts) —— 详见 路由章节。要点:
- 路径穿越与符号链接逃逸防护(realpath 包含性检查,403)。
- 弱 ETag、
If-None-Match→ 304、Range→ 206 / 416。 index(默认index.html)、maxAge(默认 3600)、cacheTtl(默认 1000ms 元数据缓存)。
请求超时
ZebraOptions.requestTimeout(毫秒)为单个请求设置截止时间:
const z = new Zebra({ requestTimeout: 5_000 });requestTimeout 必须是大于零的有限数值,非法值在构造阶段抛出 RangeError,小数毫秒仍受支持。 session.ttl(默认 30 分钟)及其优先级更高的别名 sessionTtl 遵循相同约束。 gracePeriod(默认 10,000 毫秒)必须是非负有限数值,允许为零。
- 到期后请求被中止,客户端收到 504
request_timeout(Problem+Json,detail.limit为毫秒数)。 - handler 可在
req.signal上监听abort提前停止后台工作;signal在客户端断开时同样触发(来自 Bun 原始Request.signal)。 - 后台工作不会因为超时被杀死(会继续在后台跑完),但它能通过
req.signal感知取消。默认不启用——不设置就没有截止时间与 abort 接线。