# H3Event

> H3Event, carries incoming request, prepared response and context.

With each HTTP request, H3 internally creates an `H3Event` object and passes it though event handlers until sending the response.

<read-more></read-more>

An event is passed through all the lifecycle hooks and composable utils to use it as context.

**Example:**

```js
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.

```js [app.mjs]
import { logRequest } from "./tracing.mjs";

app.get("/", (event) => {
  request.waitUntil(logRequest(request));
  return "OK";
});
```

```js [tracing.mjs]
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,
    }),
  });
}
```

> [!TIP]
> To release per-request resources (timers, upstream connections, file handles) once the event is fully over — on every runtime — use the [`onDispose(event, cb)`](/utils/response#ondisposeevent-cb) utility.

## `H3Event` Properties

### `H3Event.app?`

Access to the H3 [application instance](/guide/api/h3).

### `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 parameters
- `matchedRoute`: Matched router route object.
- `sessions`: Cached session data.
- `basicAuth`: Basic authentication data.

### `H3Event.req`
Incoming HTTP request info based on native [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) with additional runtime addons (see [srvx docs](https://srvx.h3.dev/guide/handler#extended-request-context)).

```ts
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](https://developer.mozilla.org/en-US/docs/Web/API/URL).

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

  return "OK";
});
```

#### Pathname encoding

`event.url.pathname` is the path in its wire encoding, with one exception: an escape that is <u>needlessly</u> there is dropped.

An escape is needless when every decoding consumer — a proxy, a filesystem lookup, a handler calling `decodeURIComponent` — reads it as its literal, while H3's matchers compare the two as different strings. Left alone, that gap lets `/%61dmin` slip past an `/admin` guard and still reach the `/admin` route, or reach a catch-all handler that decodes it downstream. So H3 decodes exactly those escapes, once, before routing. Nothing else is touched:

| Request | `event.url.pathname` | Why |
| --- | --- | --- |
| `/%61dmin` | `/admin` | Unreserved escape, needlessly encoded |
| `/a%2eb` | `/a.b` | Unreserved escape, needlessly encoded |
| `/a%21b` | `/a!b` | Decodes downstream, needlessly encoded |
| `/%40handle` | `/@handle` | Decodes downstream, needlessly encoded |
| `/a/%2e%2e/b` | `/b` | The URL parser resolved the dot segment first |
| `/x%2fy` | `/x%2fy` | Separator, must stay encoded |
| `/x%5cy` | `/x%5cy` | Separator, must stay encoded |
| `/100%25` | `/100%25` | Decoding it would expose a nested escape |
| `/a%20b` | `/a%20b` | The URL serializer re-encodes a space anyway |
| `/caf%C3%A9` | `/caf%C3%A9` | The URL serializer re-encodes non-ASCII anyway |

The decoded set is every escape whose character survives WHATWG path serialization unchanged, minus `%2f` and `%25`: the [RFC 3986 §2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3) unreserved set (`ALPHA` / `DIGIT` / `-` / `.` / `_` / `~`), which is equivalent to its literal per [§6.2.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.2), plus `!`, `$`, `&`, `'`, `(`, `)`, `*`, `+`, `,`, `:`, `;`, `=`, `@`, `[`, `]` and `|`. Anything else is left alone because decoding it cannot survive the round trip (`%20`, `%5E`, `%7B`, non-ASCII), would change how many segments the path has (`%2f`, `%5c`), would delete the character outright (`%09` and the other C0 controls), or would expose a nested escape (`%25`).

That is wider than [`decodeURI`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI), which preserves all of RFC 3986's reserved set (`; / ? : @ & = + $ ,`) — of which only `/` is structural in an already-parsed path. Routes like `/@handle` or `/resource:action` are ordinary, so a guard protecting one must not be walkable past by its escaped spelling (`/%40handle`).

Route matching, `use()` matchers and your own `event.url.pathname` checks therefore all compare one and the same string. `event.req.url` always keeps the original wire encoding, so for a non-canonical path the two disagree: read the path from `event.url`, and never re-derive it from `event.req.url` — slicing one by an offset taken from the other is how mount prefixes and proxy targets desync.

Canonicalization happens in the `H3Event` constructor, so it covers every event, including one built by `mockEvent()` or a standalone `handler.fetch()`.

Every escape that survives is **opaque** — treat it that way:

> [!WARNING]
> Never decode `event.url.pathname` yourself. Decoding 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 })`](/utils/request#getrouterparamsevent-opts-decode), which decodes everything else but keeps encoded separators encoded. To canonicalize a path for a scope check, use [`resolveDotSegments`](/utils/security#resolvedotsegments).

A route param therefore can never contain a path separator the router did not match on: `%2f` and `%5c` stay encoded, so `/a%2fb` and `/a%5cb` are one segment (matching the route `/:id`, not `/a/:id`).

Requests with malformed percent-encoding (such as `/foo%` or `/%ZZ`) have no canonical form to decode to and are rejected with a `400 Bad Request` before any handler runs. Set the [`allowMalformedURL`](/guide/api/h3#h3-options) app option to receive the raw pathname instead.

### `H3Event.res`

Prepared HTTP response status and headers.

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

  return "OK";
});
```

<read-more></read-more>
