H3Event
每个 HTTP 请求,H3 会在内部创建一个 H3Event 对象,并将其传递给事件处理程序,直到发送响应。
事件会经过所有的生命周期钩子和可组合工具,用作上下文。
示例:
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";
});
export async function logRequest(request) {
await fetch("https://telemetry.example.com", {
method: "POST",
body: JSON.stringify({
method: request.method,
url: request.url,
ip: request.ip,
}),
});
}
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 | /b | URL 解析器先解析了点段 |
/x%2fy | /x%2fy | 分隔符,必须保持编码 |
/x%5cy | /x%5cy | 分隔符,必须保持编码 |
/100%25 | /100%25 | 解码会暴露嵌套转义 |
/a%20b | /a%20b | URL 序列化器无论如何都会重新编码空格 |
/caf%C3%A9 | /caf%C3%A9 | URL 序列化器无论如何都会重新编码非 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";
});