H3 请求体与查询参数
读请求体在 v2 里变简单了:大部分场景直接用 event.req.json() 或 event.req.text() 就够。需要「自动判断类型」「带校验」「限制大小」时,才用这一节的工具。
三个读体工具
| 工具 | 行为 | 什么时候用 |
|---|---|---|
event.req.json() / .text() / .formData() |
原生方法,类型不对直接抛错 | 你已经知道 Content-Type |
readBody(event, options?) |
按 Content-Type 自动选 JSON 或 URL 编码表单 | 一个接口收多种格式 |
readValidatedBody(event, validate) |
读 + 校验一步到位 | 需要 zod / valibot 之类的 schema |
readBody 的规则值得记清楚,因为它和很多人预期的不同:
import { H3, readBody } from "h3";
const app = new H3().post("/", async (event) => {
// 默认按 JSON 解析;若 Content-Type 是 application/x-www-form-urlencoded 则按表单解析
const body = await readBody(event);
return { received: body };
});
它不会自动识别 multipart/form-data。 文件上传必须显式指定类型,否则你拿到的是解析失败的 JSON 或者空对象:
app.post("/upload", async (event) => {
// 必须显式声明,readBody 不会从请求头猜 multipart
const form = await readBody(event, { type: "formData" });
const file = form.get("file"); // 类型是 File 或 null
return { name: file instanceof File ? file.name : null };
});
另外,v2 的读体工具在没有请求体时不再抛错(GET 请求、空 body),而是返回空值。所以别再写 if (!body) return 400 来判断「有没有 body」,用 Content-Length 或 event.req.body 更准。
带校验地读:readValidatedBody
它接受两种校验器:普通函数,或任何符合 Standard Schema 的库(zod、valibot 都符合):
import { H3, readValidatedBody } from "h3";
import { z } from "zod";
// schema 里的类型会推导到 body 上,不用手写 interface
const userSchema = z.object({
name: z.string().min(3).max(20),
age: z.number().int().positive(),
});
const app = new H3().post("/users", async (event) => {
const body = await readValidatedBody(event, userSchema);
return { created: body.name };
});
校验失败时默认抛出一个带 400 的错误。要改错误文案,用第三个参数的 onError:
import * as v from "valibot";
app.post("/users", async (event) => {
const body = await readValidatedBody(
event,
v.object({ name: v.pipe(v.string(), v.minLength(3)) }),
{
// issues 是校验库给的明细,summarize 把它压成一行人类可读的文本
onError: ({ issues }) => ({
statusText: "参数不合法",
message: v.summarize(issues),
}),
},
);
return body;
});
💡 「Standard Schema」是一套跨库的校验器接口约定。只要库实现了它,H3 就能直接用,这也是为什么 zod 和 valibot 的写法几乎一样。换库不用改调用方式。
限制请求体大小:assertBodySize
大小限制的执行方式是这一节最值得理解的一点:它边读边数,不是先读完再检查。
import { H3, assertBodySize } from "h3";
const app = new H3().post("/", async (event) => {
// 上限 10MB:字节数在流过时累计,一超就抛 413
assertBodySize(event, 10 * 1024 * 1024);
const data = await event.req.formData();
return { ok: true };
});
具体行为分三种情况:
| 情况 | 结果 |
|---|---|
Content-Length 老实写了且超限 |
读取前直接 413 |
Content-Length 撒谎(写小了) |
读到超限那一刻 413 |
同时带 Content-Length 和 Transfer-Encoding |
400(请求走私防护,RFC 7230) |
| handler 根本没读 body | 不计数,也就不会 413 |
最后一行有实际含义:如果你在中间件里调了 assertBodySize,但路由 handler 只读 query 不读 body,那个超大的请求体不会被拒绝。要「一进来就挡」,得用 bodyLimit() 这个中间件配合读取(6.9),或者自己显式读一次。
QUERY 方法专用:两个头工具
QUERY 是 RFC 10008 定义的方法(2.2 讲过它的定位)。它有两个专属工具,都在做「协商」这件事。
appendAcceptQuery 告诉客户端「我支持哪些查询格式」:
import { appendAcceptQuery } from "h3";
app.query("/search", (event) => {
appendAcceptQuery(event, ["application/sql;charset=UTF-8", "application/jsonpath"]);
// 响应头变成:Accept-Query: application/sql;charset="UTF-8", application/jsonpath
return { rows: [] };
});
注意输出的细节:参数值会被加上引号(charset="UTF-8"),因为 RFC 8941 的结构化字段要求如此。手写这个头很容易漏引号导致客户端解析失败。
requireContentType 校验客户端发来的格式:
import { H3, requireContentType, readBody } from "h3";
const app = new H3().query("/search", async (event) => {
// 只接受 SQL 与 JSONPath;不匹配就抛错,不会走到下面
requireContentType(event, ["application/sql", "application/jsonpath"]);
const query = await readBody(event, { type: "text" });
return { query };
});
它抛出三种不同的错误,正好对应三种失败原因:
| 状态码 | 触发条件 |
|---|---|
400 |
缺 Content-Type 头 |
422 |
Content-Type 存在但格式畸形 |
415 |
类型不在允许列表里 |
允许列表支持通配:* 与 */* 匹配一切,application/* 匹配该大类的任意子类型。写成固定值更安全——通配等于放弃校验。
排查表
| 现象 | 最可能原因 | 处理 |
|---|---|---|
| 上传接口拿到空对象 | readBody 不自动识别 multipart |
显式传 { type: "formData" } |
| 超限请求没被拦 | handler 没读 body,计数没触发 | 用 bodyLimit 中间件或显式读取 |
| 读 body 报「body already used」 | 前面已经读过一次 | 用 event.req.clone() 先复制 |
| 400 但不知道错在哪 | Content-Length 与 Transfer-Encoding 同时出现 |
检查客户端/代理是否重复设头 |
| QUERY 接口 415 | 客户端 Content-Type 不在允许列表 | 检查 requireContentType 参数 |
| 校验错误文案是英文默认值 | 没传 onError |
见上文 zod/valibot 示例 |
拿到的 Accept-Query 客户端不认 |
手写了头,漏了引号 | 改用 appendAcceptQuery |
延伸阅读
- QUERY 方法的语义与适用场景:见 2.2 路由
- 请求体只能消费一次的底层原因:见 4.2 H3Event 事件对象
- 完整的 QUERY 服务端实现:见 7.2 HTTP QUERY 方法实战
- 大小限制与安全中间件:见 6.9 更多工具

TRAE-AI编程


更多建议: