请求

H3 请求工具。

#正文

#assertBodySize(event, limit)

断言请求体大小在指定限制范围内。

该限制会在读取请求体时执行,而不是通过预先缓冲实现:请求会由 srvx 的 limitRequestBody 进行包装,该函数会在数据流动时统计字节数,并在累计总数超过 limit 的瞬间中止请求,同时抛出一个 413 {@link HTTPError}(错误通过 createError 注入)。这样既能保证字节数准确(即使 Content-Length 虚报较小,也能在数据流传输过程中被捕获),又无需将请求体保存在内存中或阻塞流式处理器。

如果真实的 Content-Length 已经超过限制,请求会立即以 413 被拒绝;同时携带 Content-LengthTransfer-Encoding 的请求会以 400 被拒绝(请求走私,RFC 7230)。

由于限制是在消费请求体时执行的,分块传输或长度未知的请求体发生溢出时,会在处理器读取请求体时暴露,而不是在处理器执行前返回 413;如果处理器从未读取请求体,则不会对其进行计数。

示例:

app.post("/", async (event) => {
  assertBodySize(event, 10 * 1024 * 1024); // 10MB
  const data = await event.req.formData();
});

#readBody(event, options?)

读取请求体并尝试使用 JSON.parse 或 URLSearchParams 解析。

默认情况下,请求体会被解析为 JSON(当 Content-Typeapplication/x-www-form-urlencoded 时,则回退为 URL 编码解析)。其他请求体类型(例如 multipart/form-data)必须通过 options.type 显式启用,且不会根据请求头自动检测。

示例:

app.post("/", async (event) => {
  const body = await readBody(event);
});

示例:

app.post("/upload", async (event) => {
  const body = await readBody(event, { type: "formData" });
});

#readValidatedBody(event, validate)

尝试通过 readBody 读取请求体,然后使用提供的验证模式或函数进行验证,验证失败抛出错误,成功返回结果。

你可以使用一个简单的函数验证请求体,或者使用与标准架构兼容的库如 zod 来定义验证模式。

示例:

function validateBody(body: any) {
  return typeof body === "object" && body !== null;
}
app.post("/", async (event) => {
  const body = await readValidatedBody(event, validateBody);
});

示例:

import { z } from "zod";
const objectSchema = z.object({
  name: z.string().min(3).max(20),
  age: z.number({ coerce: true }).positive().int(),
});
app.post("/", async (event) => {
  const body = await readValidatedBody(event, objectSchema);
});

示例:

import * as v from "valibot";
app.post("/", async (event) => {
  const body = await readValidatedBody(
    event,
    v.object({
      name: v.pipe(v.string(), v.minLength(3), v.maxLength(20)),
      age: v.pipe(v.number(), v.integer(), v.minValue(1)),
    }),
    {
      onError: ({ issues }) => ({
        statusText: "自定义验证错误",
        message: v.summarize(issues),
      }),
    },
  );
});

#查询(HTTP QUERY 方法)

用于 HTTP QUERY 方法(RFC 10008) 的工具:声明资源接受的查询格式,并验证请求的 Content-Type

#appendAcceptQuery(event, mediaTypes)

通过设置 Accept-Query 响应标头,声明资源接受的查询格式(RFC 10008,HTTP QUERY 方法)。

媒体类型会被序列化为 结构化字段 列表:基本媒体类型会变为令牌,任何 ;name=value 参数都会以带引号的字符串形式输出。

示例:

app.query("/search", (event) => {
  appendAcceptQuery(event, ["application/sql;charset=UTF-8", "application/jsonpath"]);
  // Accept-Query: application/sql;charset="UTF-8", application/jsonpath
  return handleSearch(event);
});

#requireContentType(event, acceptedTypes)

断言请求中存在 Content-Type,且其值是接受的媒体类型之一,同时遵循 RFC 10008 对 HTTP QUERY 方法的要求。

抛出:

  • 如果缺少 Content-Type 标头,则抛出 400 Bad Request

  • 如果 Content-Type 标头格式错误,则抛出 422 Unprocessable Content

  • 如果媒体类型不被接受,则抛出 415 Unsupported Media Type

接受的类型可以使用通配符:* / */* 匹配任何内容,type/* 匹配 type 的任意子类型。

示例:

app.query("/search", async (event) => {
  requireContentType(event, ["application/sql", "application/jsonpath"]);
  const body = await readBody(event, { type: "text" });
  // ...
});

#缓存

#handleCacheHeaders(event, opts)

检查请求缓存标头(If-None-MatchIf-Modified-Since),并添加缓存标头(Last-Modified、ETag、Cache-Control)。

注意:默认会添加 public,但绝不会与调用方提供的 private/no-store 指令同时存在,因此传入 cacheControls: ["private"] 不会再生成相互矛盾的 public, private

#更多请求工具

#assertMethod(event, expected, allowHead?)

断言传入请求的方法是否为预期类型,使用 isMethod 进行检查。

如果方法不允许,将抛出 405 错误并包含 Allow 响应头列出允许的方法,符合 RFC 9110 的要求。

如果 allowHeadtrue,且预期方法为 GET,则允许 HEAD 请求通过。

示例:

app.get("/", (event) => {
  assertMethod(event, "GET");
  // 处理 GET 请求,否则抛出 405 错误
});

#getQuery(event)

从请求 URL 获取解析后的查询字符串对象。

要访问原始(未解析的)查询字符串,例如使用 qs 等自定义解析器解析嵌套查询,请直接使用 event.url.search

示例:

app.get("/", (event) => {
  const query = getQuery(event); // { key: "value", key2: ["value1", "value2"] }
  const rawQuery = event.url.search; // "?key=value&key2=value1&key2=value2"
});

#getRequestHost(event, opts: { xForwardedHost? })

获取请求的主机名。

如果 xForwardedHosttrue,则优先使用 x-forwarded-host 请求头(若存在)。

如果未找到主机头,将返回空字符串。

**安全性:**返回的主机名反映了客户端提供的 Host(或 X-Forwarded-Host)请求头,并且可能被伪造。除非已在上游固定或验证 Host 值(例如使用预期主机名的允许列表,或使用会覆盖该值的反向代理),否则不要将其用于安全决策(CSRF/来源检查、缓存键、生成发送给其他用户的绝对链接)。

示例:

app.get("/", (event) => {
  const host = getRequestHost(event); // "example.com"
});

#getRequestIP(event)

尝试从传入请求中获取客户端 IP 地址。

默认情况下,地址来自 event.req.ip:连接对端,或在服务器配置为信任上游代理时从转发链中解析出的客户端(例如 srvx 的 trustProxy)。

如果 xForwardedFortrue,则在请求头存在时改为返回 x-forwarded-for 请求头中的第一个条目。

如果无法确定 IP,则返回 undefined

安全性:xForwardedFor 默认不启用,因为第一个条目属于客户端输入。代理通常会将内容_追加_到链中(nginx 的 $proxy_add_x_forwarded_for、大多数 CDN 以及 h3 自身的 {@link proxy} 工具都是如此),因此客户端发送的值会保留在链的左侧,而这正是该选项返回的值——这会让任何调用方都能选择自己的地址,从而绕过 IP 允许列表、速率限制、地理位置检查和审计日志。启用该选项还会_覆盖_ event.req.ip,丢弃服务器已经正确解析出的地址。更好的做法是将服务器配置为信任你的代理(srvx 的 trustProxy 会从右侧开始遍历链,跳过受信任的跃点),并保持此选项关闭;只有当你控制的上游始终在每个请求中覆盖 x-forwarded-for 时,才应启用它。

示例:

app.get("/", (event) => {
  const ip = getRequestIP(event); // "192.0.2.0"
});

#getRequestProtocol(event, opts: { xForwardedProto? })

获取请求协议。

如果 xForwardedPrototrue,则使用 x-forwarded-proto 请求头(若存在)。当该请求头包含以逗号分隔的协议列表时,将使用第一个条目。

注意:此请求头默认未启用(默认为 false),因为客户端可能伪造它。只有当应用运行在会设置此请求头的可信反向代理或 CDN 后面时,才应启用它。此默认值已调整为与 getRequestHostxForwardedHost)和 getRequestIPxForwardedFor)保持一致。

如果无法确定协议,则默认为 "http"。

示例:

app.get("/", (event) => {
  const protocol = getRequestProtocol(event); // "https"
});

#getRequestURL(event, opts: { xForwardedHost?, xForwardedProto? })

生成完整的传入请求 URL。

如果 xForwardedHosttrue,则使用 x-forwarded-host 请求头(若存在)。

如果 xForwardedPrototrue,则使用 x-forwarded-proto 请求头(若存在)。

**安全性:**返回 URL 的 .origin.host 来源于客户端提供的 Host(或 X-Forwarded-Host)请求头,并且可能被伪造。除非已在上游固定或验证 Host 值(例如使用预期主机名的允许列表,或使用会覆盖该值的反向代理),否则不要将它们用于安全决策(CSRF/来源检查、缓存键、生成发送给其他用户的绝对链接)。.pathname.search 并非来源于可伪造的主机,但仍属于不可信的客户端输入——请根据其最终使用场景对其进行验证或编码(例如文件系统查找、HTML 输出、下游查询)。

示例:

app.get("/", (event) => {
  const url = getRequestURL(event); // "https://example.com/path"
});

#getRouterParam(event, name, opts: { decode? })

根据名称获取匹配的路由参数。

如果 decode 选项为 true,则会解码匹配的路由参数(类似于 decodeURIComponent),但编码后的路径分隔符(%2f%5c)会保持编码状态,因此解码绝不会重新引入路由未匹配到的 /\

示例:

app.get("/", (event) => {
  const param = getRouterParam(event, "key");
});

#getRouterParams(event, opts: { decode? })

获取匹配的路由参数集合。

默认情况下,参数会按照其在 URL 路径中的原样返回,仍保持百分号编码。

decode: true 时,每个参数会被解码一次(类似于 decodeURIComponent),但编码后的路径分隔符(%2f%5c,无论处于哪一层 %25 嵌套深度)会保持编码状态,因此解码绝不会重新引入路由从未匹配到的 /\

单次解码不等同于“完全解码”:%25XX 会解码为字面文本 %XX,因此结果仍可能包含百分号转义序列,包括点号段(%252e%252e -> %2e%2e)和控制字符(%2500 -> %00)。不要再次解码结果:第二次解码会将其还原为路由和中间件层从未看到的路径遍历(../)和分隔符。请将返回的字符串视为最终结果,并按原样进行验证。

示例:

app.get("/", (event) => {
  const params = getRouterParams(event); // { key: "value" }
});

示例:

// GET /files/%252e%252e/x
app.get("/files/**:rest", (event) => {
  getRouterParams(event); // { rest: "%252e%252e/x" }
  getRouterParams(event, { decode: true }); // { rest: "%2e%2e/x" } — 仍保持编码,请勿再次解码
});

#getValidatedQuery(event, validate)

获取经过验证函数验证后的请求 URL 查询参数。

你可以使用一个简单的函数来验证查询对象,或者使用一个兼容标准架构的库,如 zod 来定义架构。

示例:

app.get("/", async (event) => {
  const query = await getValidatedQuery(event, (data) => {
    return "key" in data && typeof data.key === "string";
  });
});

示例:

import { z } from "zod";
app.get("/", async (event) => {
  const query = await getValidatedQuery(
    event,
    z.object({
      key: z.string(),
    }),
  );
});

示例:

import * as v from "valibot";
app.get("/", async (event) => {
  const params = await getValidatedQuery(
    event,
    v.object({
      key: v.string(),
    }),
    {
      onError: ({ issues }) => ({
        statusText: "自定义验证错误",
        message: v.summarize(issues),
      }),
    },
  );
});

#getValidatedRouterParams(event, validate)

获取匹配的路由参数并使用验证函数验证。

如果 decode 选项为 true,参数会按照 {@link getRouterParams} 中所述的方式解码一次——路径分隔符会保持编码状态,其他转义序列只解码一层,经过验证的值仍可能包含 %XX。请按原样进行验证;不要再次解码。

你可以使用一个简单的函数来验证参数对象,或者使用与标准模式兼容的库(如 zod)来定义模式。

示例:

app.get("/:key", async (event) => {
  const params = await getValidatedRouterParams(event, (data) => {
    return "key" in data && typeof data.key === "string";
  });
});

示例:

import { z } from "zod";
app.get("/:key", async (event) => {
  const params = await getValidatedRouterParams(
    event,
    z.object({
      key: z.string(),
    }),
  );
});

示例:

import * as v from "valibot";
app.get("/:key", async (event) => {
  const params = await getValidatedRouterParams(
    event,
    v.object({
      key: v.pipe(v.string(), v.picklist(["route-1", "route-2", "route-3"])),
    }),
    {
      decode: true,
      onError: ({ issues }) => ({
        statusText: "自定义验证错误",
        message: v.summarize(issues),
      }),
    },
  );
});

#isMethod(event, expected, allowHead?)

检查传入请求的方法是否为预期类型。

如果 allowHeadtrue,且预期方法为 GET,则允许 HEAD 请求通过。

示例:

app.get("/", (event) => {
  if (isMethod(event, "GET")) {
    // 处理 GET 请求
  } else if (isMethod(event, ["POST", "PUT"])) {
    // 处理 POST 或 PUT 请求
  }
});

#requestWithBaseURL(req, base, options: { url?: URL })

创建一个轻量级请求代理,从 URL 路径名中移除基础路径。

options.url 是要从中移除 base 的已解析请求 URL,用于替代解析 req.url。如果存在 event,请始终传入 event.url:对于非规范路径,它包含父级用来匹配 base 的规范化形式,而 req.url 仍然保留线上的形式;使用从其中一个 URL 的偏移量去截取另一个 URL,正是挂载前缀发生不同步的原因。

#requestWithURL(req, url)

创建一个轻量级请求代理,仅覆盖 URL。

避免克隆原始请求(无需 new Request() 分配)。

#toRequest(input, options?)

将输入转换为网络 Request

如果输入是相对 URL,则会根据 host 请求头将其规范化为完整路径。

如果输入已经是 Request 且未提供选项,则将原样返回。

安全性:host 请求头属于客户端输入。它仅用于合成 URL 的权限部分(缺失或格式错误时回退到 localhost),绝不会扩展到路径中,并且会忽略 x-forwarded-proto,因此协议始终为 http。请传入绝对 URL 来控制来源。

#getRequestFingerprint(event, opts)

获取传入请求的唯一指纹。