H3Event

H3Event,承载传入的请求、已准备的响应和上下文。

每个 HTTP 请求,H3 会在内部创建一个 H3Event 对象,并将其传递给事件处理程序,直到发送响应。

Read more in 请求生命周期.

事件会经过所有的生命周期钩子和可组合工具,用作上下文。

示例:

app.get("/", async (event) => {
  // 记录 HTTP 请求
  console.log(`[${event.req.method}] ${event.req.url}`);

  // 解析的 URL 和查询参数
  const searchParams = event.url.searchParams;

  // 尝试读取请求的 JSON body
  const jsonBody = await event.req.json().catch(() => {});

  return "OK";
});

H3Event 方法

H3Event.waitUntil

告知运行时有一个未完成的操作,在对应的 Promise 解析前不应该关闭。

import { logRequest } from "./tracing.mjs";

app.get("/", (event) => {
  request.waitUntil(logRequest(request));
  return "OK";
});
要在每种运行时中,在事件完全结束后释放每个请求的资源(计时器、上游连接、文件句柄),请使用 onDispose(event, cb) 工具函数。

H3Event 属性

H3Event.app?

访问 H3 应用实例

H3Event.context

上下文是一个包含关于请求的任意信息的对象。

你可以将自定义属性存储在 event.context 中,以便在各种工具间共享。

已知上下文键:

  • context.params:匹配的路由参数。
  • middlewareParams:匹配的中间件参数。
  • matchedRoute:匹配的路由对象。
  • sessions:缓存的会话数据。
  • basicAuth:基本认证数据。

H3Event.req

基于原生的 Web Request 的传入 HTTP 请求信息,并包含额外的运行时扩展(参见 srvx 文档)。

app.get("/", async (event) => {
  const url = event.req.url;
  const method = event.req.method;
  const headers = event.req.headers;

  // (注意:请求体只能使用一次,可以用以下任一方法)
  const bodyStream = await event.req.body;
  const textBody = await event.req.text();
  const jsonBody = await event.req.json();
  const formDataBody = await event.req.formData();

  return "OK";
});

H3Event.url

访问完整解析后的请求 URL

app.get("/", (event) => {
  const { pathname, search, searchParams } = event.url;

  return "OK";
});

路径名编码

event.url.pathname 是其在线路上的编码形式,但有一个例外:多余的转义会被删除。

当每个解码使用方——代理、文件系统查找、调用 decodeURIComponent 的处理程序——都将某个转义读取为其字面值,而 H3 的匹配器却将两者比较为不同的字符串时,该转义就是多余的。若不处理这一差异,/%61dmin 就可以绕过 /admin 防护并仍然到达 /admin 路由,或者到达下游对其进行解码的捕获所有处理程序。因此,H3 会在路由之前,仅对这些转义执行一次解码。其他内容均不会改动:

请求event.url.pathname原因
/%61dmin/admin非保留字符转义,不必要的编码
/a%2eb/a.b非保留字符转义,不必要的编码
/a%21b/a!b会在下游解码,不必要的编码
/%40handle/@handle会在下游解码,不必要的编码
/a/%2e%2e/b/bURL 解析器先解析了点段
/x%2fy/x%2fy分隔符,必须保持编码
/x%5cy/x%5cy分隔符,必须保持编码
/100%25/100%25解码会暴露嵌套转义
/a%20b/a%20bURL 序列化器无论如何都会重新编码空格
/caf%C3%A9/caf%C3%A9URL 序列化器无论如何都会重新编码非 ASCII

经过解码的集合包含所有字符在 WHATWG 路径序列化过程中保持不变的转义,但不包括 %2f%25:即 RFC 3986 §2.3 中的非保留字符集合(ALPHA / DIGIT / - / . / _ / ~),根据 §6.2.2.2,它们等同于字面字符;此外还包括 !$&'()*+,:;=@[]|。其他内容均保持不变,因为解码后无法在往返过程中保留(%20%5E%7B、非 ASCII),会改变路径的段数(%2f%5c),会直接删除字符(%09 和其他 C0 控制字符),或者会暴露嵌套转义(%25)。

这比 decodeURI 解码范围更广,后者会保留 RFC 3986 的整个保留字符集合(; / ? : @ & = + $ ,),其中只有 / 在已经解析的路径中具有结构意义。像 /@handle/resource:action 这样的路由是普通路由,因此保护它们的防护不应因其转义形式(/%40handle)而被绕过。

因此,路由匹配、use() 匹配器和你自己的 event.url.pathname 检查都会比较同一个字符串。event.req.url 始终保留原始的线路编码,所以对于非规范路径,两者会不一致:从 event.url 读取路径,不要再从 event.req.url 重新推导路径——使用其中一个字符串的偏移量去截取另一个字符串,正是挂载前缀和代理目标发生不同步的原因。

规范化发生在 H3Event 构造函数中,因此它涵盖所有事件,包括由 mockEvent() 或独立的 handler.fetch() 构建的事件。

每个保留下来的转义都是不透明的——请以这种方式处理:

切勿自行解码 event.url.pathname。解码可能会重新引入路由和中间件从未看到的 /..,当该值传递给文件系统或上游 URL 时,这会形成路径遍历漏洞。要以解码形式读取路由参数,请使用 getRouterParams(event, { decode: true }),它会解码其他所有内容,但会保持已编码的分隔符为编码状态。要为作用域检查规范化路径,请使用 resolveDotSegments

因此,路由参数永远不会包含路由器未进行匹配的路径分隔符:%2f%5c 会保持编码状态,因此 /a%2fb/a%5cb 是一个段(匹配路由 /:id,而不是 /a/:id)。

包含格式错误百分号编码的请求(例如 /foo%/%ZZ)没有可供解码的规范形式,并会在任何处理程序运行前被拒绝,并返回 400 Bad Request。设置应用选项 allowMalformedURL 可接收原始路径名。

H3Event.res

已准备的 HTTP 响应状态和头信息。

app.get("/", (event) => {
  event.res.status = 200;
  event.res.statusText = "OK";
  event.res.headers.set("x-test", "works");

  return "OK";
});
Read more in 准备响应.