H3 请求体与查询参数

2026-10-30 17:48 更新

读请求体在 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 更多工具
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号