## File: README.md

# H3



[](https://npmjs.com/package/h3)
[](https://npm.chart.dev/h3)



H3 (pronounced as /eɪtʃθriː/, like h-3) is a minimal h(ttp) framework built for high performance and portability.

> [!NOTE]
> You are on the v2 active branch. Check out [tree/v1](https://github.com/h3js/h3/tree/v1) and [v1.h3.dev](https://v1.h3.dev/) for legacy v1.

👉 [Documentation](https://h3.dev)

## Contribution


  Local development

- Clone this repository
- Install the latest LTS version of [Node.js](https://nodejs.org/en/)
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
- Install dependencies using `pnpm install`
- Run tests using `pnpm dev` or `pnpm test`





## License



Published under the [MIT](https://github.com/h3js/h3/blob/main/LICENSE) license.
Made by [@pi0](https://github.com/pi0) and [community](https://github.com/h3js/h3/graphs/contributors) 💛

<a href="https://github.com/h3js/h3/graphs/contributors">

</a>





---

_🤖 auto updated with [automd](https://automd.unjs.io)_

---

## File: docs/1.guide/1.basics/1.lifecycle.md

---
icon: icon-park-outline:handle-round
---

# Request Lifecycle

> H3 dispatches incoming web requests to final web responses.

Below is an overview of what happens in a H3 server from when an HTTP request arrives until a response is generated.

## 1. Incoming Request

When An HTTP request is made by Browser or [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), server fetch handler receives a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object.

```mermaid
%%{init: {'theme':'neutral'}}%%
flowchart LR
  A1["`fetch(request)`"] --> A2["`server.fetch(request)`"]

click A2 "/guide/api/h3#h3fetch"
```

> [!TIP]
> ​[💥 Srvx](https://srvx.h3.dev) provides unified `server.fetch` interface and adds [Node.js compatibility](https://srvx.h3.dev/guide/node).

## 2. Accept Request

H3 Initializes an [`H3Event`](/guide/api/h3event) instance from incoming request, calls [`onRequest`](/guide/api/h3#global-hooks) global hook and finally [`H3.handler`](/guide/api/h3#h3handler) with the initialized event.

```mermaid
%%{init: {'theme':'neutral'}}%%
flowchart LR
  B1["`new H3Event(request)`"] --> B2["`onRequest(event)`"] --> B3["`h3.handler(event)`"]

click B1 "/guide/api/h3event"
click B2 "/guide/api/h3#global-hooks"
click B3 "/guide/api/h3#apphandler"
```

## 3. Dispatch Request

H3 [matches route](/guide/basics/routing) based on `request.url` and `request.method`, calls global [middleware](/guide/basics/middleware) and finally matched route handler function with event.

```mermaid
%%{init: {'theme':'neutral'}}%%
sequenceDiagram
    participant MiddlewareA as Middleware1(event, next)
    participant MiddlewareB as Middleware2(event, next)
    participant Route as RouteHandler(event)

    MiddlewareA->>+MiddlewareB: await next()
    MiddlewareB->>+Route: await next()
    Route-->>-MiddlewareB: rawBody
    MiddlewareB-->>-MiddlewareA: rawBody

```

> [!TIP]
> 🚀 Internally, H3 uses srvx `FastURL` instead of `new URL(req.url).pathname`.

## 4. Send Response

H3 [converts](/guide/basics/response#response-types) returned value and [prepared headers](/guide/basics/response#preparing-response) into a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), calls [`onResponse`](/guide/api/h3#global-hooks) global hook and finally returns response back to the server fetch handler.

```mermaid
%%{init: {'theme':'neutral'}}%%
flowchart LR
 D1["Returned Value => Response"] --> D2["`onResponse(response)`"] --> D3["Response"]

click D1 "/guide/basics/response"
click D2 "/guide/api/h3#global-hooks"
```

---

## File: docs/1.guide/1.basics/2.routing.md

---
icon: solar:routing-bold
---

# Routing

> Each request is matched to one (most specific) route handler.

## Adding Routes

You can register route [handlers](/guide/basics/handler) to [H3 instance](/guide/api/h3) using [`H3.on`](/guide/api/h3#h3on), [`H3.[method]`](/guide/api/h3#h3method), or [`H3.all`](/guide/api/h3#h3all).

> [!TIP]
> Router is powered by [🌳 Rou3](https://github.com/h3js/rou3), an ultra-fast and tiny route matcher engine.

**Example:** Register a route to match requests to the `/hello` endpoint with HTTP **GET** method.

- Using [`H3.[method]`](/guide/api/h3#h3method)

  ```js
  app.get("/hello", () => "Hello world!");
  ```

- Using [`H3.on`](/guide/api/h3#h3on)

  ```js
  app.on("GET", "/hello", () => "Hello world!");
  ```

You can register multiple event handlers for the same route with different methods:

```js
app
  .get("/hello", () => "GET Hello world!")
  .post("/hello", () => "POST Hello world!")
  .all("/hello", () => "Any other method!");
```

You can also use [`H3.all`](/guide/api/h3#h3all) method to register a route accepting any HTTP method:

```js
app.all("/hello", (event) => `This is a ${event.req.method} request!`);
```

## HEAD Requests

Following [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-head), `HEAD` requests automatically match the corresponding `GET` route and run its handler, but the response body is omitted (only the headers and status are sent). You don't need to register a separate `HEAD` handler:

```js
app.get("/hello", () => "Hello world!");

// HEAD /hello → 200 with the same headers as GET, but an empty body
```

Register an explicit `HEAD` handler when you want to override this — for example, to skip computing the body:

```js
app.head("/hello", (event) => {
  event.res.headers.set("content-length", "12");
  return null;
});
```

An explicit `head()` route always takes precedence over the automatic `GET` fallback.

## HTTP `QUERY` Method

H3 supports the [HTTP `QUERY` method (RFC 10008)](https://www.rfc-editor.org/rfc/rfc10008) as a first-class method. `QUERY` is like `GET` — **safe, idempotent, and cacheable** — but carries a request body (with a `Content-Type`), closing the long-standing "GET with a body" gap. It's ideal for complex read operations where filters don't fit in a URL.

Register a `QUERY` handler with `app.query()` (or `app.on("QUERY", …)`) and read the request body as usual:

```js
import { readBody } from "h3";

app.query("/search", async (event) => {
  const criteria = await readBody(event); // read the query body
  return runSearch(criteria);
});
```

Because `QUERY` carries an attacker-controllable body, [body-size limits](/utils/request#assertbodysizeevent-limit) apply just like `POST`.

Two utilities help implement the RFC:

- [`requireContentType(event, acceptedTypes)`](/utils/request#requirecontenttypeevent-acceptedtypes) — assert the request `Content-Type` (`400`/`415`/`422`).
- [`appendAcceptQuery(event, mediaTypes)`](/utils/request#appendacceptqueryevent-mediatypes) — advertise accepted query formats via the `Accept-Query` response header.

> [!NOTE]
> `QUERY` is treated like `GET` for [conditional caching](/utils/request#handlecacheheadersevent-opts) (`304` responses via `handleCacheHeaders`), and [`proxy`](/utils/proxy) forwards it **with** its body. Unlike `GET`, `QUERY` is **not** CORS-safelisted, so browsers send a preflight — if you pass an explicit `methods` allowlist to [`handleCors`](/utils/security#handlecorsevent-options), include `"QUERY"`.

::read-more{to="/examples/handle-query"}
See the [HTTP `QUERY` method example](/examples/handle-query) for a runnable `/books` resource that validates the `Content-Type` and advertises a cacheable `GET` alternative.
::

## Route Patterns

A route pattern is a **pathname**, not a URL: the same shape as `event.url.pathname`, plus [rou3](https://github.com/h3js/rou3) syntax. [`H3.on`](/guide/api/h3#h3on), [`H3.[method]`](/guide/api/h3#h3method), [`H3.all`](/guide/api/h3#h3all), [`H3.use(route, ...)`](/guide/api/h3#h3use), [`H3.mount`](/guide/api/h3#h3mount) and [`removeRoute`](/utils/more#removerouteapp-method-route) normalize it identically, so a middleware registered with the same string as a route always guards that route.

Normalization rules:

- A leading `/` is added when missing (`"hello"` → `/hello`).
- A URL is **rejected** (`app.get("http://example.com/admin")` throws). An authority is never silently dropped: `//admin` registers as the two-segment path `//admin`, not as `/`.
- `.` and `..` segments resolve exactly as the URL parser resolves them in a request path (`/admin/../admin` → `/admin`).
- Characters that a request pathname always carries percent-encoded are encoded: space, non-ASCII, control characters, `"`, `#`, `<`, `>` and `` ` ``. So `app.get("/café")` registers `/caf%C3%A9` — what the browser actually sends.
- Needless escapes are decoded to the literal that the request pathname is canonicalized to (`/%40handle` → `/@handle`, see [Security utils](/utils/security)).

Characters that carry rou3 meaning are left exactly as written, including the escape `\` (never valid in a request pathname). To match `?`, `{`, `}` or `^` **literally**, write it percent-encoded:

```js
app.get("/u/:id?", () => "optional param"); // rou3 syntax, kept as written
app.get("/x%3Fy", () => "literal ?"); // matches the path a client sends for /x?y
```

> [!NOTE]
> Non-ASCII text mixes freely with dynamic syntax — `app.get("/café/:id")` registers `/caf%C3%A9/:id` and matches `/café/42` — with two exceptions, both from the encoded form reaching rou3's own syntax. A **param name** must be ASCII (`[\w-]`): `/:naïve` becomes `/:na%C3%AFve`, which rou3 reads as a param named `na` followed by the literal `%C3%AFve`. And inside a `(...)` group, only literal text and alternation survive encoding (`(café|thé)` works); a **character class** does not, since `[é]` becomes `[%C3%A9]` — write the encoded alternation `(?:%C3%A9)` instead.

## Dynamic Routes

You can define dynamic route parameters using `:` prefix:

```js
// [GET] /hello/Bob => "Hello, Bob!"
app.get("/hello/:name", (event) => {
  return `Hello, ${event.context.params.name}!`;
});
```

Instead of named parameters, you can use `*` for unnamed **optional** parameters:

```js
app.get("/hello/*", (event) => `Hello!`);
```

## Wildcard Routes

Adding `/hello/:name` route will match `/hello/world` or `/hello/123`. But it will not match `/hello/foo/bar`.
When you need to match multiple levels of sub routes, you can use `**` prefix:

```js
app.get("/hello/**", (event) => `Hello ${event.context.params._}!`);
```

This will match `/hello`, `/hello/world`, `/hello/123`, `/hello/world/123`, etc.

> [!NOTE]
> Param `_` will store the full wildcard content as a single string.

## Route Meta

You can define optional route meta when registering them, accessible from any middleware.

```js
import { H3 } from "h3";

const app = new H3();

app.use((event) => {
  console.log(event.context.matchedRoute?.meta); // { auth: true }
});

app.get("/", (event) => "Hi!", { meta: { auth: true } });
```

::read-more{to="/guide/basics/handler#meta"}
It is also possible to add route meta when defining them using `defineHandler` object syntax.
::

---

## File: docs/1.guide/1.basics/3.middleware.md

---
icon: mdi:middleware-outline
---

# Middleware

> Intercept request, response and errors using H3 middleware.

> [!IMPORTANT]
> We recommend using composable utilities whenever possible. Global middleware can complicate application logic, making it less predictable and harder to understand.

Global middleware run on each request before route handler and act as wrappers to intercept request, response and errors.

:read-more{to="/guide/basics/lifecycle#\_3-dispatch-request" title="Request Lifecycle"}

You can register global middleware to [app instance](/guide/api/h3) using the [`H3.use`](/guide/api/h3#h3use).

**Example:** Register a global middleware that logs every request.

```js
app.use((event) => {
  console.log(event);
});
```

**Example:** Register a global middleware that matches certain requests.

```js
app.use(
  "/blog/**",
  (event, next) => {
    console.log("[alert] POST request on /blog paths!");
  },
  {
    method: "POST",
    // match: (event) => event.req.method === "POST",
  },
);
```

You can register middleware with `next` argument to intercept return values of next middleware and handler.

```js
app.use(async (event, next) => {
  const rawBody = await next();
  // [intercept response]
  return rawBody;
});
```

Example below, always responds with `Middleware 1`.

```js
app
  .use(() => "Middleware 1")
  .use(() => "Middleware 2")
  .get("/", "Hello");
```

> [!IMPORTANT]
> If middleware returns a value other than `undefined` or the result of `next()`, it immediately intercepts request handling and sends a response.

When adding routes, you can register middleware that only run with them.

```js
import { basicAuth } from "h3";

app.get(
  "/secret",
  (event) => {
    /* ... */
  },
  {
    middleware: [basicAuth({ password: "test" })],
  },
);
```

For convenience, H3 provides middleware factory functions `onRequest`, `onResponse`, and `onError`:

```js
import { onRequest, onResponse, onError } from "h3";

app.use(
  onRequest((event) => {
    console.log(`[${event.req.method}] ${event.url.pathname}`);
  }),
);

app.use(
  onResponse((response, event) => {
    console.log(`[${event.req.method}] ${event.url.pathname} ~>`, response.status);
  }),
);

app.use(
  onError((error, event) => {
    console.log(`[${event.req.method}] ${event.url.pathname} !! ${error.message}`);
  }),
);
```

---

## File: docs/1.guide/1.basics/4.handler.md

---
icon: mdi:function
---

# Event Handlers

> An event handler is a function that receives an H3Event and returns a response.

You can define typed event handlers using `defineHandler`.

```js
import { H3, defineHandler } from "h3";

const app = new H3();

const handler = defineHandler((event) => "Response");

app.get("/", handler);
```

> [!NOTE]
> Using `defineHandler` is optional.
> You can instead, simply use a function that accepts an [`H3Event`](/guide/api/h3event) and returns a response.

The callback function can be sync or async:

```js
defineHandler(async (event) => "Response");
```

## Object Syntax

### middleware

You can optionally register some [middleware](/guide/basics/middleware) to run with event handler to intercept request, response or errors.

```js
import { basicAuth } from "h3";

defineHandler({
  middleware: [basicAuth({ password: "test" })],
  handler: (event) => "Hi!",
});
```

:read-more{to="/guide/basics/response" title="Response Handling"}

:read-more{to="/guide/api/h3event" }

### meta

You can define optional route meta attached to handlers, and access them from any other middleware.

```js
import { H3, defineHandler } from "h3";

const app = new H3();

app.use((event) => {
  console.log(event.context.matchedRoute?.meta); // { tag: "admin" }
});

app.get("/admin/**", defineHandler({
  meta: { tag: "admin" },
  handler: (event) => "Hi!",
})
```

::read-more{to="/guide/basics/routing#route-meta"}
It is also possible to add route meta when registering them to app instance.
::

## Handler `.fetch`

Event handlers defined with `defineHandler`, can act as a web handler without even using [H3](/guide/api/h3) class.

```js
const handler = defineHandler(async (event) => `Request: ${event.req.url}`);

const response = await handler.fetch("http://localhost/");
console.log(response, await response.text());
```

## Lazy Handlers

You can define lazy event handlers using `defineLazyEventHandler`. This allow you to define some one-time logic that will be executed only once when the first request matching the route is received.

A lazy event handler must return an event handler.

```js
import { defineLazyEventHandler } from "h3";

defineLazyEventHandler(async () => {
  await initSomething(); // Will be executed only once
  return (event) => {
    return "Response";
  };
});
```

This is useful to define some one-time logic such as configuration, class initialization, heavy computation, etc.

Another use-case is lazy loading route chunks:

```js [app.mjs]
import { H3, defineLazyEventHandler } from "h3";

const app = new H3();

app.all(
  "/route",
  defineLazyEventHandler(() => import("./route.mjs").then((mod) => mod.default)),
);
```

```js [route.mjs]
import { defineHandler } from "h3";

export default defineHandler((event) => "Hello!");
```

## Converting to Handler

There are situations that you might want to convert an event handler or utility made for Node.js or another framework to H3.
There are built-in utils to do this.

### From Web Handlers

Request handlers with [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) => [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) signuture can be converted into H3 event handlers using `fromWebHandler` utility or [H3.mount](/guide/api/h3#h3mount).

```js
import { H3, fromWebHandler } from "h3";

export const app = new H3();

const webHandler = (request) => new Response("👋 Hello!");

// Using fromWebHandler utility
app.all("/web", fromWebHandler(webHandler));

// Using simple wrapper
app.all("/web", (event) => webHandler(event.req));

// Using app.mount
app.mount("/web", webHandler);
```

### From Node.js Handlers

If you have a legacy request handler with `(req, res) => {}` syntax made for Node.js, you can use `fromNodeHandler` to convert it to an h3 event handler.

> [!IMPORTANT]
> Node.js event handlers can only run within Node.js server runtime!

```js
import { H3, fromNodeHandler } from "h3";

// Force using Node.js compatibility (also works with Bun and Deno)
import { serve } from "h3/node";

export const app = new H3();

const nodeHandler = (req, res) => {
  res.end("Node handlers work!");
};

app.get("/web", fromNodeHandler(nodeHandler));
```

---

## File: docs/1.guide/1.basics/5.response.md

---
icon: tabler:json
---

# Sending Response

> H3 automatically converts any returned value into a web response.

Values returned from [Event Handlers](/guide/basics/handler) are automatically converted to a web [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) by H3.

**Example:** Simple event handler function.

```js
const handler = defineHandler((event) => ({ hello: "world" }));
```

H3 smartly converts handler into:

```js
const handler = (event) =>
  new Response(JSON.stringify({ hello: "world" }), {
    headers: {
      "content-type": "application/json;charset=UTF-8",
    },
  });
```

> [!TIP]
> 🚀 H3 uses srvx `FastResponse` internally to optimize performances in Node.js runtime.

If the returned value from event handler is a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) or from an [async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function), H3 will wait for it to resolve before sending the response.

If an error is thrown, H3 automatically handles it with error handler.

:read-more{to="/guide/basics/error" title="Error Handling"}

## Preparing Response

Before returning a response in main handler, you can prepare response headers and status using [`event.res`](/guide/api/h3event#eventres).

```js
defineHandler((event) => {
  event.res.status = 200;
  event.res.statusText = "OK";
  event.res.headers.set("Content-Type", "text/html");
  return "<h1>Hello, World</h1>";
});
```

> [!NOTE]
> If a full [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response) value is returned, prepared status is discarded and headers will be merged/overridden. For performance reasons, it is best to only set headers only from final Response in this case.

> [!NOTE]
> If an Error happens, prepared status and headers will be discarded. The recommended way to include headers in error responses is via `new HTTPError({ headers })`. As a last resort for headers that need to be set implicitly before the error is known (e.g., CORS), you can use `event.res.errHeaders` — these will be merged into error responses automatically.

## Response Types

H3 smartly converts JavaScript values into web [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response).

### JSON Serializable Value

Returning a [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) serializable value (**object**, **array**, **number** or **boolean**), it will be stringified using [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) and sent with default `application/json` content-type.

**Example:**

```ts
app.get("/", (event) => ({ hello: "world" }));
```

> [!TIP]
> Returned objects with `.toJSON()` property can customize serialization behavior. Check [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) for more info.

### String

Returning a string value, sends it as plain text body.

> [!NOTE]
> If not setting `content-type` header, it can default to `text/plain;charset=UTF-8`.

**Example:** Send HTML response.

```ts
app.get("/", (event) => {
  event.res.headers.set("Content-Type", "text/html;charset=UTF-8");
  return "<h1>hello world</h1>";
});
```

You can also use `html` utility as shortcut. Interpolated values in the tagged template are automatically HTML-escaped to help prevent XSS.

```js
import { html, raw } from "h3";

// Tagged template: interpolations are escaped
app.get("/hello/:name", (event) => html`<h1>hello ${event.context.params.name}</h1>`);

// Trusted markup: sent as-is
app.get("/", () => html(raw("<h1>hello world</h1>")));
```

> [!IMPORTANT]
> Calling `html()` with a plain string escapes the whole string (a warning is logged if escaping changed it). Use the tagged template for dynamic values, or wrap trusted markup with `raw()`.

### `Response`

Returning a web [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response), sends-it as final response.

**Example:**

```ts
app.get("/", (event) => new Response("Hello, world!", { headers: { "x-powered-by": "H3" } }));
```

> [!IMPORTANT]
> When sending a `Response`, any [prepared headers](#preparing-response) that set before, will be merged as default headers. `event.res.{status,statusText}` will be ignored. For performance reasons, it is best to only set headers only from final `Response`.
>
> If the returned `Response` has an error status (`>= 400`), prepared headers are discarded — same as for a thrown error — and only `event.res.errHeaders` are merged. Success and redirect responses (`< 400`) receive all prepared headers, so `setCookie()` before returning a `302` works as expected.

### `ReadableStream` or `Readable`

Returning a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) or Node.js [`Readable`](https://nodejs.org/api/stream.html#readable-streams) sends it as stream.

### `ArrayBuffer` or `Uint8Array` or `Buffer`

Send binary [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) or node [Buffer](https://nodejs.org/api/buffer.html#buffer).

`content-length` header will be automatically set.

### `Blob`

Send a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) as stream.

`Content-type` and `Content-Length` headers will be automatically set.

### `File`

Send a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) as stream.

`Content-type`, `Content-Length` and `Content-Disposition` headers will be automatically set.

## Special Types

Some less commonly possible values for response types.

### `null` or `undefined`

Sends a response with empty body.

> [!TIP]
> If there is no `return` statement in event handler, it is same as `return undefined`.

### `Error`

Retuning an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) instance will send it.

> [!IMPORTANT]
> It is better to `throw` errors instead of returning them. This allows proper propagation from any nested utility.

:read-more{to="/guide/basics/error" title="Error Handling"}

### `BigInt`

Value will be sent as stringified version of BigInt number.

> [!NOTE]
> Returning a JSON object, does not allows BigInt serialization. You need to implement `.toJSON`. Check [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) for more info.

### `Symbol` or `Function`

**Returning Symbol or Function has undetermined behavior.** Currently, H3 sends a string-like representation of unknown Symbols and Functions but this behavior might be changed to throw an error in the future versions.

There are some internal known Symbols H3 internally uses:

- `Symbol.for("h3.notFound")`: Indicate no route is found to throw a 404 error.
- `Symbol.for("h3.handled")`: Indicate request is somehow handled and H3 should not continue (Node.js specific).

---

## File: docs/1.guide/1.basics/6.error.md

---
icon: tabler:error-404
---

# Error Handling

> Send errors by throwing an HTTPError.

H3 captures all possible errors during [request lifecycle](/guide/basics/lifecycle).

## `HTTPError`

You can create and throw HTTP errors using `HTTPError` with different syntaxes.

```js
import { HTTPError } from "h3";

app.get("/error", (event) => {
  // Using message and details
  throw new HTTPError("Invalid user input", { status: 400 });

  // Using HTTPError.status(code)
  throw HTTPError.status(400, "Bad Request");

  // Using single object
  throw new HTTPError({
    status: 400,
    statusText: "Bad Request",
    message: "Invalid user input",
    data: { field: "email" },
    body: { date: new Date().toJSON() },
    headers: {},
  });
});
```

This will end the request with `400 - Bad Request` status code and the following JSON response:

```json
{
  "date": "2025-06-05T04:20:00.0Z",
  "status": 400,
  "statusText": "Bad Request",
  "message": "Invalid user input",
  "data": {
    "field": "email"
  }
}
```

### `HTTPError` Fields

- `status`: HTTP status code in the range 200–599.
- `statusText`: HTTP status text to be sent in the response header.
- `message`: Error message to be included in the JSON body.
- `data`: Additional data to be attached under the `data` key in the error JSON body.
- `body`: Additional top-level properties to be attached in the error JSON body.
- `headers`: Additional HTTP headers to be sent in the error response.
- `cause`: The original error object that caused this error, useful for tracing and debugging.
- `unhandled`: Indicates whether the error was thrown for unknown reasons. See [Unhandled Errors](#unhandled-errors).

> [!TIP]
> The recommended way to include headers in error responses is to use `new HTTPError({ headers })`:
>
> ```js
> throw new HTTPError({
>   status: 400,
>   message: "Invalid input",
>   headers: { "x-request-id": requestId },
> });
> ```
>
> When an error is thrown, any [prepared headers](/guide/basics/response#preparing-response) set via `event.res.headers` are **not** included in the error response. As a last resort for headers that need to be set implicitly before the error is known (e.g., CORS headers), you can use `event.res.errHeaders`. Built-in utilities like `handleCors` automatically set both.

> [!IMPORTANT]
> Error `statusText` should be short (max 512 to 1024 characters) and only include tab, spaces or visible ASCII characters and extended characters (byte value 128–255). Prefer `message` in JSON body for extended message.

## Unhandled Errors

Any error that occurs during calling [request lifecycle](/guide/basics/lifecycle) without using `HTTPError` will be processed as an _unhandled_ error.

```js
app.get("/error", (event) => {
  // This will cause an unhandled error.
  throw new Error("Something went wrong");
});
```

> [!TIP]
> For enhanced security, H3 hides certain fields of unhandled errors (`data`, `body`, `stack` and `message`) in JSON response.

## Catching Errors

Using global [`onError`](/guide/api/h3#global-hooks) hook:

```js
import { H3, onError } from "h3";

// Globally handling errors
const app = new H3({
  onError: (error) => {
    console.error(error);
  },
});
```

Using [`onError` middleware](/guide/basics/middleware) to catch errors.

```js
import { onError } from "h3";

// Handling errors using middleware
app.use(
  onError((error, event) => {
    console.error(error);
  }),
);
```

> [!TIP]
> When using nested apps, global hooks of sub-apps will not be called. Therefore it is better to use `onError` middleware.

---

## File: docs/1.guide/1.basics/7.nested-apps.md

---
icon: material-symbols-light:layers-outline
---

# Nested Apps

> H3 has a native `mount` method for adding nested sub-apps to the main instance.

Typically, H3 projects consist of several [Event Handlers](/guide/basics/handler) defined in one or multiple files (or even [lazy loaded](/guide/basics/handler#lazy-handlers) for faster startup times).

It is sometimes more convenient to combine multiple `H3` instances or even use another HTTP framework used by a different team and mount it to the main app instance. H3 provides a native [`.mount`](/guide/api/h3#h3mount) method to facilitate this.

## Nested H3 Apps

H3 natively allows mounting sub-apps. When mounted, sub-app routes and middleware are **merged** with the base url prefix into the main app instance.

```js
import { H3, serve } from "h3";

const nestedApp = new H3()
  .use((event) => {
    event.res.headers.set("x-api", "1");
  })
  .get("/**:slug", (event) => ({
    pathname: event.url.pathname,
    slug: event.context.params?.slug,
  }));

const app = new H3().mount("/api", nestedApp);
```

In the example above, when fetching the `/api/test` URL, `pathname` will be `/api/test` (the real path), and `slug` will be `/test` (wildcard param).

> [!NOTE]
> Global config and hooks won't be inherited from the nested app. Consider always setting them from the main app.

## Nested Web Standard Apps

Mount a `.fetch` compatible server instance like [Hono](https://hono.dev/) or [Elysia](https://elysiajs.com/) under the base URL.

> [!NOTE]
> Base prefix will be removed from `request.url` passed to the mounted app.

```js
import { H3 } from "h3";
import { Hono } from "hono";
import { Elysia } from "elysia";

const app = new H3()
  .mount(
    "/elysia",
    new Elysia().get("/test", () => "Hello Elysia!"),
  )
  .mount(
    "/hono",
    new Hono().get("/test", (c) => c.text("Hello Hono!")),
  );
```

> [!TIP]
> Similarly, you can mount an H3 app in [Hono](https://hono.dev/docs/api/hono#mount) or [Elysia](https://elysiajs.com/patterns/mount#mount-1).

---

## File: docs/1.guide/900.api/1.h3.md

---
icon: material-symbols:bolt-rounded
---

# H3

> H3 class is the core of server.

You can create a new H3 app instance using `new H3()`:

```js
import { H3 } from "h3";

const app = new H3({/* optional config */});
```

## `H3` Methods

### `H3.request`

A [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-compatible function allowing to fetch app routes.

- Input can be a relative path, [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL), or [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).
- Returned value is a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) promise.

```ts
const response = await app.request("/");
console.log(response, await response.text());
```

### `H3.fetch`

Similar to `H3.request` but only accepts one `(req: Request)` argument for cross runtime compatibility.

### `H3.on`

Register route handler for specific HTTP method.

```js
const app = new H3().on("GET", "/", () => "OK");
```

:read-more{to="/guide/basics/routing" title="Routing"}

### `H3.[method]`

Register route handler for specific HTTP method (shortcut for `app.on(method, ...)`).

```js
const app = new H3().get("/", () => "OK");
```

### `H3.all`

Register route handler for all HTTP methods.

```js
const app = new H3().all("/", () => "OK");
```

### `H3.use`

Register a global [middleware](/guide/basics/middleware).

```js
const app = new H3()
  .use((event) => {
    console.log(`request: ${event.req.url}`);
  })
  .all("/", () => "OK");
```

:read-more{to="/guide/basics/middleware" title="Middleware"}

### `H3.register`

Register a H3 plugin to extend app.

:read-more{to="/guide/advanced/plugins" title="Plugins"}

### `H3.handler`

An H3 [event handler](/guide/basics/handler) useful to compose multiple H3 app instances.

**Example:** Nested apps.

```js
import { H3, serve, redirect, withBase } from "h3";

const nestedApp = new H3().get("/test", () => "/test (sub app)");

const app = new H3()
  .get("/", (event) => redirect(event, "/api/test"))
  .all("/api/**", withBase("/api", nestedApp.handler));

serve(app);
```

### `H3.mount`

Using `.mount` method, you can register a sub-app with prefix.

:read-more{to="/guide/basics/nested-apps" title="Nested Apps"}

## `H3` Options

You can pass global app configuration when initializing an app.

Supported options:

- `debug`: Displays debugging stack traces in HTTP responses (potentially dangerous for production!).
- `silent`: When enabled, console errors for unhandled exceptions will not be displayed.
- `allowMalformedURL`: When enabled, requests with a malformed percent-encoded URL path (e.g. `/foo%`, `/%ZZ`) are allowed through with the raw pathname instead of being rejected with a `400 Bad Request` (the default).
- `plugins`: (see [plugins](/guide/advanced/plugins) for more information)

> [!IMPORTANT]
> Enabling `debug` option, sends important stuff like stack traces in error responses. Only enable during development.

### Global Hooks

When initializing an H3 app, you can register global hooks:

- `onError`
- `onRequest`
- `onResponse`

These hooks are called for every request and can be used to add global logic to your app such as logging, error handling, etc.

```js
const app = new H3({
  onRequest: (event) => {
    console.log("Request:", event.req.url);
  },
  onResponse: (response, event) => {
    console.log("Response:", event.url.pathname, response.status);
  },
  onError: (error, event) => {
    console.error(error);
  },
});
```

> [!IMPORTANT]
> Global hooks only run from main H3 app and **not** sub-apps. Use [middleware](/guide/basics/middleware) for more flexibility.

## `H3` Properties

### `H3.config`

Global H3 instance config.

---

## File: docs/1.guide/900.api/2.h3event.md

---
icon: material-symbols:data-object-rounded
---

# 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{to="/guide/basics/lifecycle" title="Request Lifecycle"}

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 _needlessly_ 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{to="/guide/basics/response#preparing-response" title="Preparing Response"}