H3Event
With each HTTP request, H3 internally creates an H3Event object and passes it though event handlers until sending the response.
An event is passed through all the lifecycle hooks and composable utils to use it as context.
Example:
app.get("/", async (event) => {
// Log HTTP request
console.log(`[${event.req.method}] ${event.req.url}`);
// Parsed URL and query params
const searchParams = event.url.searchParams;
// Try to read request JSON body
const jsonBody = await event.req.json().catch(() => {});
return "OK";
});
H3Event Methods
H3Event.waitUntil
Tell the runtime about an ongoing operation that shouldn't close until the promise resolves.
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) utility.H3Event Properties
H3Event.app?
Access to the H3 application instance.
H3Event.context
The context is an object that contains arbitrary information about the request.
You can store your custom properties inside event.context to share across utils.
Known context keys:
context.params: Matched router parameters.middlewareParams: Matched middleware parametersmatchedRoute: Matched router route object.sessions: Cached session data.basicAuth: Basic authentication data.
H3Event.req
Incoming HTTP request info based on native Web Request with additional runtime addons (see srvx docs).
app.get("/", async (event) => {
const url = event.req.url;
const method = event.req.method;
const headers = event.req.headers;
// (note: you can consume body only once with either of this)
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
Access to the full parsed request URL.
app.get("/", (event) => {
const { pathname, search, searchParams } = event.url;
return "OK";
});
Pathname decoding
event.url.pathname is percent-decoded once, while event.req.url keeps the original encoding exactly as it arrived on the wire. H3 decodes eagerly so that route matching and any pathname-based middleware always compare the same normalized value — otherwise a request to /%61dmin would slip past an /admin guard and still reach the /admin route.
Decoding is a single decodeURI pass, and the result is re-serialized by the URL parser. This means only unreserved escapes visibly decode:
| Request | event.url.pathname | Why |
|---|---|---|
/%41 | /A | Unreserved escape, decodes |
/a%2eb | /a.b | Unreserved escape, decodes |
/a/%2e%2e/b | /b | Decoded .. collapses during re-parse |
/x%2fy | /x%2fy | Structural, kept encoded |
/100%25 | /100%25 | Structural, kept encoded |
/a%20b | /a%20b | Re-encoded by the URL serializer |
/caf%C3%A9 | /caf%C3%A9 | Re-encoded by the URL serializer |
Either way, a route param can never contain a path separator that the router did not match on: %2f stays encoded, and %5c decodes to \, which the URL parser then normalizes into a real / the router splits on (/a%5cb matches the route /a/:id).
event.url.pathname again. A second decodeURIComponent can reintroduce a / or .. that routing and middleware never saw, which is a path traversal vector when the value reaches a filesystem or an upstream URL. To read a route param in decoded form, use getRouterParams(event, { decode: true }), which decodes everything else but keeps encoded separators encoded.Requests with malformed percent-encoding (such as /foo% or /%ZZ) are rejected with a 400 Bad Request before any handler runs. Set the allowMalformedURL app option to receive the raw pathname instead.
H3Event.res
Prepared HTTP response status and headers.
app.get("/", (event) => {
event.res.status = 200;
event.res.statusText = "OK";
event.res.headers.set("x-test", "works");
return "OK";
});