h3
Version:
1,201 lines (1,200 loc) • 67.9 kB
text/typescript
import { CookieSerializeOptions, DynamicEventHandler, ErrorDetails, EventHandler, EventHandlerObject, EventHandlerRequest, EventHandlerResponse, EventHandlerWithFetch, FetchableObject, H3, H3$1, H3Config, H3Event, H3EventContext, H3Plugin, H3RouteMeta, HTTPError, HTTPEvent, HTTPHandler, HTTPMethod, InferEventInput, MaybePromise as MaybePromise$1, Middleware, TypedRequest } from "./h3.mjs";
import { NodeServerRequest, NodeServerResponse, ServerRequest, ServerRequestContext } from "srvx";
import { Hooks, Hooks as WebSocketHooks, Message as WebSocketMessage, Peer, Peer as WebSocketPeer } from "crossws";
declare function isEvent(input: any): input is H3Event;
/**
* Checks if the input is an object with `{ req: Request }` signature.
* @param input - The input to check.
* @returns True if the input is `{ req: Request }`
*/
declare function isHTTPEvent(input: any): input is HTTPEvent;
/**
* Gets the context of the event, if it does not exists, initializes a new context on `req.context`.
*/
declare function getEventContext<T extends ServerRequestContext | H3EventContext>(event: HTTPEvent | H3Event): T;
declare function mockEvent(_request: string | URL | Request, options?: RequestInit & {
h3?: H3EventContext;
}): H3Event;
/** The Standard Schema interface. */
interface StandardSchemaV1<Input = unknown, Output = Input> {
/** The Standard Schema properties. */
readonly "~standard": Props<Input, Output>;
}
/** The Standard Schema properties interface. */
interface Props<Input = unknown, Output = Input> {
/** The version number of the standard. */
readonly version: 1;
/** The vendor name of the schema library. */
readonly vendor: string;
/** Validates unknown input values. */
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
/** Inferred types associated with the schema. */
readonly types?: Types<Input, Output> | undefined;
}
/** The result interface of the validate function. */
type Result<Output> = SuccessResult<Output> | FailureResult;
/** The result interface if validation succeeds. */
interface SuccessResult<Output> {
/** The typed output value. */
readonly value: Output;
/** The non-existent issues. */
readonly issues?: undefined;
}
/** The result interface if validation fails. */
interface FailureResult {
/** The issues of failed validation. */
readonly issues: ReadonlyArray<Issue>;
}
/** The issue interface of the failure output. */
interface Issue {
/** The error message of the issue. */
readonly message: string;
/** The path of the issue, if any. */
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
}
/** The path segment interface of the issue. */
interface PathSegment {
/** The key representing a path segment. */
readonly key: PropertyKey;
}
/** The Standard Schema types interface. */
interface Types<Input = unknown, Output = Input> {
/** The input type of the schema. */
readonly input: Input;
/** The output type of the schema. */
readonly output: Output;
}
/** Infers the output type of a Standard Schema. */
type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
type ValidateResult<T> = T | true | false | void;
type OnValidateError<Source extends string = string> = (result: FailureResult & {
_source?: Source;
}) => ErrorDetails;
declare function defineHandler<Req extends EventHandlerRequest = EventHandlerRequest, Res = EventHandlerResponse>(handler: EventHandler<Req, Res>): EventHandlerWithFetch<Req, Res>;
declare function defineHandler<Req extends EventHandlerRequest = EventHandlerRequest, Res = EventHandlerResponse>(def: EventHandlerObject<Req, Res>): EventHandlerWithFetch<Req, Res>;
type StringHeaders<T> = { [K in keyof T]: Extract<T[K], string>; };
/**
* @experimental defineValidatedHandler is an experimental feature and API may change.
*/
declare function defineValidatedHandler<RequestBody extends StandardSchemaV1, RequestHeaders extends StandardSchemaV1, RequestQuery extends StandardSchemaV1, Res extends EventHandlerResponse = EventHandlerResponse>(def: Omit<EventHandlerObject, "handler"> & {
validate?: {
body?: RequestBody;
headers?: RequestHeaders;
query?: RequestQuery;
onError?: OnValidateError;
};
handler: EventHandler<{
body: InferOutput<RequestBody>;
query: StringHeaders<InferOutput<RequestQuery>>;
}, Res>;
}): EventHandlerWithFetch<TypedRequest<InferOutput<RequestBody>, InferOutput<RequestHeaders>>, Res>;
declare function dynamicEventHandler(initial?: EventHandler | FetchableObject): DynamicEventHandler;
type MaybePromise<T> = T | Promise<T>;
declare function defineLazyEventHandler(loader: () => MaybePromise<HTTPHandler>): EventHandlerWithFetch;
declare function toEventHandler(handler: HTTPHandler | undefined): EventHandler | undefined;
declare function defineMiddleware(input: Middleware): Middleware;
declare function callMiddleware(event: H3Event, middleware: Middleware[], handler: EventHandler, index?: number): unknown | Promise<unknown>;
/**
* Converts any HTTPHandler or Middleware into Middleware.
*
* If FetchableObject or Handler returns a Response with 404 status, the next middleware will be called.
*/
declare function toMiddleware(input: HTTPHandler | Middleware | undefined): Middleware;
declare function toResponse(val: unknown, event: H3Event, config?: H3Config): Response | Promise<Response>;
declare class HTTPResponse {
#private;
body?: BodyInit | null;
constructor(body: BodyInit | null, init?: Pick<ResponseInit, "status" | "statusText" | "headers">);
get status(): number;
get statusText(): string;
get headers(): Headers;
}
type NodeHandler = (req: NodeServerRequest, res: NodeServerResponse) => unknown | Promise<unknown>;
type NodeMiddleware = (req: NodeServerRequest, res: NodeServerResponse, next: (error?: Error) => void) => unknown | Promise<unknown>;
/**
* @deprecated Since h3 v2 you can directly use `app.fetch(request, init?, context?)`
*/
declare function toWebHandler(app: H3): (request: ServerRequest, context?: H3EventContext) => Promise<Response>;
declare function fromWebHandler(handler: (request: ServerRequest, context?: H3EventContext) => Promise<Response>): EventHandler;
/**
* Convert a Node.js handler function (req, res, next?) to an EventHandler.
*
* **Note:** The returned event handler requires to be executed with h3 Node.js handler.
*/
declare function fromNodeHandler(handler: NodeMiddleware): EventHandler;
declare function fromNodeHandler(handler: NodeHandler): EventHandler;
declare function defineNodeHandler(handler: NodeHandler): NodeHandler;
declare function defineNodeMiddleware(handler: NodeMiddleware): NodeMiddleware;
/**
* Route definition options
*/
interface RouteDefinition {
/**
* HTTP method for the route, e.g. 'GET', 'POST', etc.
*/
method: HTTPMethod;
/**
* Route pattern, e.g. '/api/users/:id'
*/
route: string;
/**
* Handler function for the route.
*/
handler: EventHandler;
/**
* Optional middleware to run before the handler.
*/
middleware?: Middleware[];
/**
* Additional route metadata.
*/
meta?: H3RouteMeta;
validate?: {
body?: StandardSchemaV1;
headers?: StandardSchemaV1;
query?: StandardSchemaV1;
};
}
/**
* Define a route as a plugin that can be registered with app.register()
*
* @example
* ```js
* import { z } from "zod";
*
* const userRoute = defineRoute({
* method: 'POST',
* validate: {
* query: z.object({ id: z.string().uuid() }),
* body: z.object({ name: z.string() }),
* },
* handler: (event) => {
* return { success: true };
* }
* });
*
* app.register(userRoute);
* ```
*/
declare function defineRoute(def: RouteDefinition): H3Plugin;
/**
* Remove a route handler from the app.
*
* @example
* ```ts
* import { H3, removeRoute } from "h3";
*
* const app = new H3();
* app.get("/temp", () => "hello");
*
* removeRoute(app, "GET", "/temp"); // route removed
* ```
*/
declare function removeRoute(app: H3$1, method: HTTPMethod | Lowercase<HTTPMethod> | "", route: string): void;
/**
* Create a lightweight request proxy that overrides only the URL.
*
* Avoids cloning the original request (no `new Request()` allocation).
*/
declare function requestWithURL(req: ServerRequest, url: string): ServerRequest;
/**
* Create a lightweight request proxy with the base path stripped from the URL pathname.
*/
declare function requestWithBaseURL(req: ServerRequest, base: string): ServerRequest;
/**
* Convert input into a web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).
*
* If input is a relative URL, it will be normalized into a full path based on headers.
*
* If input is already a Request and no options are provided, it will be returned as-is.
*/
declare function toRequest(input: ServerRequest | URL | string, options?: RequestInit): ServerRequest;
/**
* Get parsed query string object from the request URL.
*
* @example
* app.get("/", (event) => {
* const query = getQuery(event); // { key: "value", key2: ["value1", "value2"] }
* });
*/
declare function getQuery<T, Event extends H3Event | HTTPEvent = HTTPEvent, _T = Exclude<InferEventInput<"query", Event, T>, undefined>>(event: Event): _T;
declare function getValidatedQuery<Event extends HTTPEvent, S extends StandardSchemaV1<any, any>>(event: Event, validate: S, options?: {
onError?: (result: FailureResult) => ErrorDetails;
}): Promise<InferOutput<S>>;
declare function getValidatedQuery<Event extends HTTPEvent, OutputT, InputT = InferEventInput<"query", Event, OutputT>>(event: Event, validate: (data: InputT) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>, options?: {
onError?: () => ErrorDetails;
}): Promise<OutputT>;
/**
* Get matched route params.
*
* If `decode` option is `true`, it will decode the matched route params (like
* `decodeURIComponent`), except encoded path separators (`%2f`, `%5c`) are kept
* encoded so decoding can never reintroduce a `/` or `\` the router never matched.
*
* @example
* app.get("/", (event) => {
* const params = getRouterParams(event); // { key: "value" }
* });
*/
declare function getRouterParams(event: HTTPEvent, opts?: {
decode?: boolean;
}): NonNullable<H3Event["context"]["params"]>;
declare function getValidatedRouterParams<Event extends HTTPEvent, S extends StandardSchemaV1>(event: Event, validate: S, options?: {
decode?: boolean;
onError?: (result: FailureResult) => ErrorDetails;
}): Promise<InferOutput<S>>;
declare function getValidatedRouterParams<Event extends HTTPEvent, OutputT, InputT = InferEventInput<"routerParams", Event, OutputT>>(event: Event, validate: (data: InputT) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>, options?: {
decode?: boolean;
onError?: () => ErrorDetails;
}): Promise<OutputT>;
/**
* Get a matched route param by name.
*
* If `decode` option is `true`, it will decode the matched route param (like
* `decodeURIComponent`), except encoded path separators (`%2f`, `%5c`) are kept
* encoded so decoding can never reintroduce a `/` or `\` the router never matched.
*
* @example
* app.get("/", (event) => {
* const param = getRouterParam(event, "key");
* });
*/
declare function getRouterParam(event: HTTPEvent, name: string, opts?: {
decode?: boolean;
}): string | undefined;
/**
*
* Checks if the incoming request method is of the expected type.
*
* If `allowHead` is `true`, it will allow `HEAD` requests to pass if the expected method is `GET`.
*
* @example
* app.get("/", (event) => {
* if (isMethod(event, "GET")) {
* // Handle GET request
* } else if (isMethod(event, ["POST", "PUT"])) {
* // Handle POST or PUT request
* }
* });
*/
declare function isMethod(event: HTTPEvent, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean): boolean;
/**
* Asserts that the incoming request method is of the expected type using `isMethod`.
*
* If the method is not allowed, it will throw a 405 error and include an `Allow`
* response header listing the permitted methods, as required by RFC 9110.
*
* If `allowHead` is `true`, it will allow `HEAD` requests to pass if the expected method is `GET`.
*
* @example
* app.get("/", (event) => {
* assertMethod(event, "GET");
* // Handle GET request, otherwise throw 405 error
* });
*/
declare function assertMethod(event: HTTPEvent, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean): void;
/**
* Get the request hostname.
*
* If `xForwardedHost` is `true`, it will use the `x-forwarded-host` header if it exists.
*
* If no host header is found, it will return an empty string.
*
* @example
* app.get("/", (event) => {
* const host = getRequestHost(event); // "example.com"
* });
*/
declare function getRequestHost(event: HTTPEvent, opts?: {
xForwardedHost?: boolean;
}): string;
/**
* Get the request protocol.
*
* If `x-forwarded-proto` header is set to "https", it will return "https". If the header contains a comma-separated list of protocols, the first entry is used. You can disable this behavior by setting `xForwardedProto` to `false`.
*
* If protocol cannot be determined, it will default to "http".
*
* @example
* app.get("/", (event) => {
* const protocol = getRequestProtocol(event); // "https"
* });
*/
declare function getRequestProtocol(event: HTTPEvent | H3Event, opts?: {
xForwardedProto?: boolean;
}): "http" | "https" | (string & {});
/**
* Generated the full incoming request URL.
*
* If `xForwardedHost` is `true`, it will use the `x-forwarded-host` header if it exists.
*
* If `xForwardedProto` is `false`, it will not use the `x-forwarded-proto` header.
*
* @example
* app.get("/", (event) => {
* const url = getRequestURL(event); // "https://example.com/path"
* });
*/
declare function getRequestURL(event: HTTPEvent | H3Event, opts?: {
xForwardedHost?: boolean;
xForwardedProto?: boolean;
}): URL;
/**
* Try to get the client IP address from the incoming request.
*
* If `xForwardedFor` is `true`, it will use the `x-forwarded-for` header if it exists.
*
* If IP cannot be determined, it will default to `undefined`.
*
* @example
* app.get("/", (event) => {
* const ip = getRequestIP(event); // "192.0.2.0"
* });
*/
declare function getRequestIP(event: HTTPEvent, opts?: {
/**
* Use the X-Forwarded-For HTTP header set by proxies.
*
* Note: Make sure that this header can be trusted (your application running behind a CDN or reverse proxy) before enabling.
*/
xForwardedFor?: boolean;
}): string | undefined;
type IterationSource<Val, Ret = Val> = Iterable<Val> | AsyncIterable<Val> | Iterator<Val, Ret | undefined> | AsyncIterator<Val, Ret | undefined> | (() => Iterator<Val, Ret | undefined> | AsyncIterator<Val, Ret | undefined>);
type IteratorSerializer<Value> = (value: Value) => Uint8Array | undefined;
/**
* Respond with an empty payload.<br>
*
* @example
* app.get("/", () => noContent());
*
* @param status status code to be send. By default, it is `204 No Content`.
*/
declare function noContent(status?: number): HTTPResponse;
/**
* Send a redirect response to the client.
*
* It adds the `location` header to the response and sets the status code to 302 by default.
*
* In the body, it sends a simple HTML page with a meta refresh tag to redirect the client in case the headers are ignored.
*
* @example
* app.get("/", () => {
* return redirect("https://example.com");
* });
*
* @example
* app.get("/", () => {
* return redirect("https://example.com", 301); // Permanent redirect
* });
*/
declare function redirect(location: string, status?: number, statusText?: string): HTTPResponse;
/**
* Redirect the client back to the previous page using the `referer` header.
*
* If the `referer` header is missing or is a different origin, it falls back to the provided URL (default `"/"`).
*
* By default, only the **pathname** of the referer is used (query string and hash are stripped)
* to prevent spoofed referers from carrying unintended parameters. Set `allowQuery: true` to preserve the query string.
*
* **Security:** The `fallback` value MUST be a trusted, hardcoded path — never use user input.
* Passing user-controlled values (e.g., query params) as `fallback` creates an open redirect vulnerability.
*
* @example
* app.post("/submit", (event) => {
* // process form...
* return redirectBack(event, { fallback: "/form" });
* });
*/
declare function redirectBack(event: H3Event, opts?: {
/** Fallback URL when referer is missing or cross-origin (default: `"/"`). **Must be a trusted, hardcoded path — never user input.** */
fallback?: string;
/** HTTP status code for the redirect (default: `302`). */
status?: number;
/** Preserve the query string from the referer URL (default: `false`). */
allowQuery?: boolean;
}): HTTPResponse;
/**
* Write `HTTP/1.1 103 Early Hints` to the client.
*
* In runtimes that don't support early hints natively, this function
* falls back to setting response headers which can be used by CDN.
*/
declare function writeEarlyHints(event: H3Event, hints: Record<string, string | string[]>): void | Promise<void>;
/**
* Iterate a source of chunks and send back each chunk in order.
* Supports mixing async work together with emitting chunks.
*
* Each chunk must be a string or a buffer.
*
* For generator (yielding) functions, the returned value is treated the same as yielded values.
*
* @param iterable - Iterator that produces chunks of the response.
* @param serializer - Function that converts values from the iterable into stream-compatible values.
* @template Value - Test
*
* @example
* return iterable(async function* work() {
* // Open document body
* yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n";
* // Do work ...
* for (let i = 0; i < 1000; i++) {
* await delay(1000);
* // Report progress
* yield `<li>Completed job #`;
* yield i;
* yield `</li>\n`;
* }
* // Close out the report
* return `</ol></body></html>`;
* });
* async function delay(ms) {
* return new Promise((resolve) => setTimeout(resolve, ms));
* }
*/
declare function iterable<Value = unknown, Return = unknown>(iterable: IterationSource<Value, Return>, options?: {
serializer: IteratorSerializer<Value | Return>;
}): HTTPResponse;
/**
* Respond with HTML content.
*
* @example
* app.get("/", () => html("<h1>Hello, World!</h1>"));
* app.get("/", () => html`<h1>Hello, ${name}!</h1>`);
*/
declare function html(strings: TemplateStringsArray, ...values: unknown[]): HTTPResponse;
declare function html(markup: string): HTTPResponse;
/**
* Advertise the query formats a resource accepts by setting the `Accept-Query`
* response header (RFC 10008, HTTP `QUERY` method).
*
* The media types are serialized as a
* [Structured Fields](https://www.rfc-editor.org/rfc/rfc8941) List: the base
* media type becomes a token and any `;name=value` parameters are emitted with
* their values as quoted strings.
*
* @example
* app.query("/search", (event) => {
* appendAcceptQuery(event, ["application/sql;charset=UTF-8", "application/jsonpath"]);
* // Accept-Query: application/sql;charset="UTF-8", application/jsonpath
* return handleSearch(event);
* });
*
* @param event The H3Event passed by the handler.
* @param mediaTypes A media type (with optional parameters) or an array of them.
*/
declare function appendAcceptQuery(event: H3Event, mediaTypes: string | string[]): void;
/**
* Assert that the request `Content-Type` is present and one of the accepted
* media types, following the requirements of RFC 10008 for the HTTP `QUERY`
* method.
*
* Throws:
*
* - `400 Bad Request` if the `Content-Type` header is missing.
*
* - `422 Unprocessable Content` if the `Content-Type` header is malformed.
*
* - `415 Unsupported Media Type` if the media type is not accepted.
*
* Accepted types may use wildcards: `*` / `*/*` match anything and
* `type/*` matches any subtype of `type`.
*
* @example
* app.query("/search", async (event) => {
* requireContentType(event, ["application/sql", "application/jsonpath"]);
* const body = await readBody(event, { type: "text" });
* // ...
* });
*
* @param event The HTTPEvent passed by the handler.
* @param acceptedTypes An accepted media type or an array of them.
* @returns The matched request media type (lower-cased, without parameters).
*/
declare function requireContentType(event: HTTPEvent, acceptedTypes: string | string[]): string;
/**
* Define a middleware that runs on each request.
*/
declare function onRequest(hook: (event: H3Event) => MaybePromise$1<void>): Middleware;
/**
* Define a middleware that runs after Response is generated.
*
* You can return a new Response from the handler to replace the original response.
*/
declare function onResponse(hook: (response: Response, event: H3Event) => unknown): Middleware;
/**
* Define a middleware that runs when an error occurs.
*
* You can return a new Response from the handler to gracefully handle the error.
*/
declare function onError(hook: (error: HTTPError, event: H3Event) => unknown): Middleware;
/**
* Define a middleware that checks whether request body size is within specified limit.
*
* If body size exceeds the limit, throws a `413` Request Entity Too Large response error.
* If you need custom handling for this case, use `assertBodySize` instead.
*
* @param limit Body size limit in bytes
* @see {assertBodySize}
*/
declare function bodyLimit(limit: number): Middleware;
interface ProxyOptions {
headers?: HeadersInit;
/**
* Header names allowed to bypass the built-in hop-by-hop denylist. Matched
* case-insensitively.
*
* This is **not** an exclusive allowlist: all ordinary request headers are
* still forwarded regardless. It only lists exceptions that force-forward a
* header the proxy would otherwise drop — e.g. `forwardHeaders: ["host"]`
* forwards the client's `host` verbatim. `filterHeaders` still wins over it.
*/
forwardHeaders?: string[];
/**
* Denylist of incoming request header names to drop before proxying.
* Header names are matched case-insensitively.
*/
filterHeaders?: string[];
/**
* Options forwarded to the underlying `fetch()` call.
*
* Upstream 3xx responses are passed through to the client by default
* (`redirect: "manual"`) rather than followed. Set
* `fetchOptions: { redirect: "follow" }` to restore following redirects — but
* note that following a redirect for a request with a streamed body can fail,
* since the body cannot be replayed once it has been consumed.
*/
fetchOptions?: RequestInit & {
duplex?: "half" | "full";
};
cookieDomainRewrite?: string | Record<string, string>;
cookiePathRewrite?: string | Record<string, string>;
/**
* Rewrite `location` and `refresh` response headers, like nginx
* `proxy_redirect`:
*
* - `true` (default): a URL whose origin matches the proxy `target` is
* rewritten to the proxy's own origin (path and query preserved), so
* client-side redirects keep flowing through the proxy instead of
* exposing the upstream host. Relative and third-party URLs are left
* untouched, as are internal (`/`-prefixed) targets, which already share
* the proxy origin.
* - A record maps URL prefixes to replacements (nginx
* `proxy_redirect <from> <to>`); the first matching prefix is replaced,
* e.g. `{ "https://upstream.example/two/": "/one/" }`. Only the explicit
* mappings apply in this mode (including for internal targets).
* - `false`: forward these headers verbatim.
*
* @default true
*/
locationRewrite?: boolean | Record<string, string>;
onResponse?: (event: H3Event, response: Response) => void | Promise<void>;
/**
* Control how a client disconnect is handled.
*
* The incoming request's abort signal (`event.req.signal`) is always forwarded
* to the proxied request, so a client disconnect aborts the upstream request
* and releases its connection. By default the resulting abort is handled
* quietly with a `499 Client Closed Request` response (never delivered, since
* the client is already gone) rather than logged as a `502` gateway error.
*
* Set this to `true` to instead let the `AbortError` propagate to your handler
* (e.g. to run cleanup). This also applies to a custom `fetchOptions.signal`,
* except when it aborts with a `TimeoutError` — timeouts always map to `504`
* (see `timeout`).
*/
propagateAbortError?: boolean;
/**
* Milliseconds to wait for the upstream response (headers) before giving up.
* On timeout the proxy responds with `504 Gateway Timeout`. The deadline is
* cleared once the upstream responds — it never cuts off a long-running
* response body stream.
*
* Because a fired timeout aborts with a `TimeoutError`, a caller-supplied
* `fetchOptions.signal` that is itself an `AbortSignal.timeout` is also
* mapped to `504` (rather than the `499` used for client disconnects) — note
* that such a signal stays armed during body streaming and can truncate it;
* prefer this option.
*/
timeout?: number;
/**
* When `true`, add `x-forwarded-*` request headers derived from the incoming
* request so the upstream learns the client and original request info:
*
* - `x-forwarded-for`: the client IP (`event.req.ip`, when available).
* - `x-forwarded-proto`: the incoming request protocol.
* - `x-forwarded-host`: the original host (incl. port).
* - `x-forwarded-port`: the original port (or the protocol default — `443` for
* https, `80` for http).
*
* Each header is only set when absent — a value already present on the
* incoming request (or set via header options) is left untouched.
*
* **Security:** because present values win, a client-supplied
* `x-forwarded-for` is forwarded verbatim and the real client IP is never
* added. On an internet-facing server (no trusted proxy in front), strip
* incoming values first with `filterHeaders: ["x-forwarded-for"]` if the
* upstream trusts this header for allowlisting, rate limiting, or logging.
*
* Note that `x-forwarded-proto`/`-host` reflect `event.url` (the server's
* resolved protocol and host), not the raw client headers — so `filterHeaders`
* does not affect them. By default the server derives these from the real
* transport and the on-the-wire `Host`, so a client cannot spoof them; they
* only follow an inbound `x-forwarded-*` header when the server is explicitly
* configured to trust an upstream proxy (e.g. srvx's `trustProxy`), which is
* the correct setup when a proxy you control sits in front.
*
* Only applied by `proxyRequest` (which forwards the incoming request);
* the lower-level `proxy` ignores this option.
*
* @default false
*/
xfwd?: boolean;
}
/**
* Proxy the incoming request to a target URL.
*
* If the `target` starts with `/`, the request is handled internally by the app router
* via `event.app.fetch()` instead of making an external HTTP request.
*
* The request body is streamed to the target without buffering. Per the Fetch
* standard, a request body can only be consumed once, so reading it beforehand
* (e.g. via `readBody()`, `readFormData()`, or body-reading middleware) locks
* the stream and proxying fails. If you need to inspect the body and still
* proxy it, read from a clone and leave the original event untouched.
*
* Upstream 3xx responses are passed through to the client by default rather than
* followed. Set `fetchOptions: { redirect: "follow" }` to follow them instead —
* but following a redirect with a streamed request body can fail, since the body
* cannot be replayed once consumed.
*
* **Security:** Never pass unsanitized user input as the `target`. Callers are
* responsible for validating and restricting the target URL (e.g. allowlisting
* hosts, blocking internal paths, enforcing protocol). Consider using
* `bodyLimit()` middleware to prevent large request bodies from consuming
* excessive resources when proxying untrusted input.
*
* @example
* app.all("/proxy", async (event) => {
* const body = await event.req.clone().json(); // read from the clone
* // ...inspect body...
* return proxyRequest(event, "/target"); // original stream still intact
* });
*/
declare function proxyRequest(event: H3Event, target: string, opts?: ProxyOptions): Promise<HTTPResponse>;
/**
* Make a proxy request to a target URL and send the response back to the client.
*
* If the `target` starts with `/`, the request is dispatched internally via
* `event.app.fetch()` (sub-request) and never leaves the process. This bypasses
* any external security layer (reverse proxy auth, IP allowlisting, mTLS).
*
* Upstream 3xx responses are passed through to the client by default rather than
* followed. Set `fetchOptions: { redirect: "follow" }` to follow them instead —
* but following a redirect with a streamed request body can fail, since the body
* cannot be replayed once consumed. (Internal sub-requests via `event.app.fetch()`
* never follow redirects.)
*
* **Limitations** (inherited from `fetch`): upstream response bodies are always
* decompressed (compression is not preserved end-to-end), the `host` header is
* rewritten to the target (preserving it via `forwardHeaders: ["host"]` works on
* Node.js but may be ignored on other runtimes), and unix sockets, TLS options,
* or connection agents require a runtime-specific escape hatch (e.g. undici's
* `dispatcher` in `fetchOptions` on Node.js). On browser and service-worker
* runtimes, `redirect: "manual"` produces an unrelayable opaque-redirect for
* external targets (a `502` is returned) — set
* `fetchOptions: { redirect: "follow" }` there.
*
* **Security:** Never pass unsanitized user input as the `target`. Callers are
* responsible for validating and restricting the target URL (e.g. allowlisting
* hosts, blocking internal paths, enforcing protocol).
*/
declare function proxy(event: H3Event, target: string, opts?: ProxyOptions): Promise<HTTPResponse>;
/**
* Get the request headers object without headers known to cause issues when proxying.
*/
declare function getProxyRequestHeaders(event: H3Event, opts?: {
host?: boolean;
forwardHeaders?: string[];
filterHeaders?: string[];
}): Record<string, string>;
/**
* Make a fetch request carrying the event's context.
*
* Behavior depends on the target:
*
* An **internal** `url` (starting with `/`) is dispatched via
* `event.app.fetch()` (sub-request) and never leaves the process. It inherits
* the incoming request's filtered headers (via `getProxyRequestHeaders`) and
* runtime metadata (`ip`, `waitUntil`, ...).
*
* An **external** `url` is sent with native `fetch(url, init)` **unchanged** —
* the event's headers and context are *not* inherited (forwarding cookies or
* authorization to arbitrary hosts would be unsafe). A streamed `init.body`
* is given `duplex: "half"` when unset, which Node's `fetch` requires.
*
* **Security:** Never pass unsanitized user input as the `url`. Callers are
* responsible for validating and restricting the URL.
*/
declare function fetchWithEvent(event: H3Event, url: string, init?: RequestInit & {
duplex?: "half" | "full";
}): Promise<Response>;
interface ReadBodyOptions {
/**
* Force a parser instead of inferring it from the request `Content-Type`.
*
* - `"json"` (default): parse as JSON.
* - `"text"`: return the raw string body.
* - `"urlencoded"`: parse as `application/x-www-form-urlencoded`.
* - `"formData"`: parse as `multipart/form-data` (or url-encoded) form data.
*/
type?: "json" | "text" | "urlencoded" | "formData";
}
/**
* Reads request body and tries to parse using JSON.parse or URLSearchParams.
*
* By default the body is parsed as JSON (falling back to URL-encoded parsing
* when the `Content-Type` is `application/x-www-form-urlencoded`). Other body
* types, such as `multipart/form-data`, must be opted into explicitly via
* `options.type` and are never auto-detected from the request headers.
*
* @example
* app.post("/", async (event) => {
* const body = await readBody(event);
* });
* @example
* app.post("/upload", async (event) => {
* const body = await readBody(event, { type: "formData" });
* });
*
* @param event H3 event passed by h3 handler
* @param options Parsing options. Set `type` to force a parser instead of
* inferring it from the request `Content-Type`.
*
* @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request body
*/
declare function readBody<T, _Event extends HTTPEvent = HTTPEvent, _T = InferEventInput<"body", _Event, T>>(event: _Event, options?: ReadBodyOptions): Promise<undefined | _T>;
declare function readValidatedBody<Event extends HTTPEvent, S extends StandardSchemaV1>(event: Event, validate: S, options?: {
onError?: (result: FailureResult) => ErrorDetails;
}): Promise<InferOutput<S>>;
declare function readValidatedBody<Event extends HTTPEvent, OutputT, InputT = InferEventInput<"body", Event, OutputT>>(event: Event, validate: (data: InputT) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>, options?: {
onError?: () => ErrorDetails;
}): Promise<OutputT>;
/**
* Asserts that request body size is within the specified limit.
*
* If body size exceeds the limit, throws a `413` Request Entity Too Large response error.
*
* @example
* app.get("/", async (event) => {
* await assertBodySize(event, 10 * 1024 * 1024); // 10MB
* const data = await event.req.formData();
* });
*
* @param event HTTP event
* @param limit Body size limit in bytes
*/
declare function assertBodySize(event: HTTPEvent, limit: number): Promise<void>;
/**
* Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs.
* @param event {HTTPEvent} H3 event or req passed by h3 handler
* @returns Object of cookie name-value pairs
* ```ts
* const cookies = parseCookies(event)
* ```
*/
declare function parseCookies(event: HTTPEvent): Record<string, string | undefined>;
/**
* Get and validate all cookies using a Standard Schema or custom validator.
*
* @example
* app.get("/", async (event) => {
* const cookies = await getValidatedCookies(event, z.object({
* session: z.string(),
* theme: z.enum(["light", "dark"]).optional(),
* }));
* });
*/
declare function getValidatedCookies<Event extends HTTPEvent, S extends StandardSchemaV1<any, any>>(event: Event, validate: S, options?: {
onError?: (result: FailureResult) => ErrorDetails;
}): Promise<InferOutput<S>>;
declare function getValidatedCookies<Event extends HTTPEvent, OutputT>(event: Event, validate: (data: Record<string, string | undefined>) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>, options?: {
onError?: () => ErrorDetails;
}): Promise<OutputT>;
/**
* Get a cookie value by name.
* @param event {HTTPEvent} H3 event or req passed by h3 handler
* @param name Name of the cookie to get
* @returns {*} Value of the cookie (String or undefined)
* ```ts
* const authorization = getCookie(request, 'Authorization')
* ```
*/
declare function getCookie(event: HTTPEvent, name: string): string | undefined;
/**
* Set a cookie value by name.
* @param event {H3Event} H3 event or res passed by h3 handler
* @param name Name of the cookie to set
* @param value Value of the cookie to set
* @param options {CookieSerializeOptions} Options for serializing the cookie
* ```ts
* setCookie(res, 'Authorization', '1234567')
* ```
*/
declare function setCookie(event: H3Event, name: string, value: string, options?: CookieSerializeOptions): void;
/**
* Remove a cookie by name.
* @param event {H3Event} H3 event or res passed by h3 handler
* @param name Name of the cookie to delete
* @param serializeOptions {CookieSerializeOptions} Cookie options
* ```ts
* deleteCookie(res, 'SessionId')
* ```
*/
declare function deleteCookie(event: H3Event, name: string, serializeOptions?: CookieSerializeOptions): void;
/**
* Get a chunked cookie value by name. Will join chunks together.
* @param event {HTTPEvent} { req: Request }
* @param name Name of the cookie to get
* @returns {*} Value of the cookie (String or undefined)
* ```ts
* const session = getChunkedCookie(event, 'Session')
* ```
*/
declare function getChunkedCookie(event: HTTPEvent, name: string): string | undefined;
/**
* Set a cookie value by name. Chunked cookies will be created as needed.
* @param event {H3Event} H3 event or res passed by h3 handler
* @param name Name of the cookie to set
* @param value Value of the cookie to set
* @param options {CookieSerializeOptions} Options for serializing the cookie
* ```ts
* setCookie(res, 'Session', '<session data>')
* ```
*/
declare function setChunkedCookie(event: H3Event, name: string, value: string, options?: CookieSerializeOptions & {
chunkMaxLength?: number;
}): void;
/**
* Remove a set of chunked cookies by name.
* @param event {H3Event} H3 event or res passed by h3 handler
* @param name Name of the cookie to delete
* @param serializeOptions {CookieSerializeOptions} Cookie options
* ```ts
* deleteCookie(res, 'Session')
* ```
*/
declare function deleteChunkedCookie(event: H3Event, name: string, serializeOptions?: CookieSerializeOptions): void;
/**
* A helper class for [server sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format)
*/
declare class EventStream {
private readonly _event;
private readonly _transformStream;
private readonly _writer;
private readonly _encoder;
private _writerIsClosed;
private _paused;
private _unsentData;
private _disposed;
private _handled;
private get _isClosed();
constructor(event: H3Event, opts?: EventStreamOptions);
/**
* Publish new event(s) for the client
*/
push(message: string): Promise<void>;
push(message: string[]): Promise<void>;
push(message: EventStreamMessage): Promise<void>;
push(message: EventStreamMessage[]): Promise<void>;
pushComment(comment: string): Promise<void>;
private _sendEvent;
private _sendEvents;
pause(): void;
get isPaused(): boolean;
resume(): Promise<void>;
flush(): Promise<void>;
/**
* Close the stream and the connection if the stream is being sent to the client
*/
close(): Promise<void>;
/**
* Triggers callback when the writable stream is closed.
* It is also triggered after calling the `close()` method.
*/
onClosed(cb: () => any): void;
send(): Promise<BodyInit>;
}
interface EventStreamOptions {
/**
* Automatically close the writable stream when the request is closed
*
* Default is `true`
*/
autoclose?: boolean;
}
/**
* See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields
*/
interface EventStreamMessage {
id?: string;
event?: string;
retry?: number;
data: string;
}
/**
* Initialize an EventStream instance for creating [server sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
*
* @experimental This function is experimental and might be unstable in some environments.
*
* @example
*
* ```ts
* import { createEventStream } from "h3";
*
* app.get("/sse", (event) => {
* const eventStream = createEventStream(event);
*
* // Send a message every second
* const interval = setInterval(async () => {
* await eventStream.push("Hello world");
* }, 1000);
*
* // cleanup the interval and close the stream when the connection is terminated
* eventStream.onClosed(async () => {
* console.log("closing SSE...");
* clearInterval(interval);
* await eventStream.close();
* });
*
* return eventStream.send();
* });
* ```
*/
declare function createEventStream(event: H3Event, opts?: EventStreamOptions): EventStream;
/**
* Append a `Server-Timing` entry to the response.
*
* Multiple calls append to the same header (comma-separated per spec).
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing
*
* @example
* app.get("/", (event) => {
* setServerTiming(event, "db", { dur: 53, desc: "Database query" });
* return { data: "..." };
* });
* // Response header: Server-Timing: db;desc="Database query";dur=53
*/
declare function setServerTiming(event: H3Event, name: string, opts?: {
dur?: number;
desc?: string;
}): void;
/**
* Measure an async operation and append the timing to the `Server-Timing` header.
*
* Uses `performance.now()` for high-resolution timing.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing
*
* @example
* app.get("/", async (event) => {
* const users = await withServerTiming(event, "db", () => fetchUsers());
* return users;
* });
* // Response header: Server-Timing: db;dur=42.5
*/
declare function withServerTiming<T>(event: H3Event, name: string, fn: () => T | Promise<T>): Promise<T>;
/**
* Make sure the status message is safe to use in a response.
*
* Allowed characters: horizontal tabs, spaces or visible ascii characters: https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2
*/
declare function sanitizeStatusMessage(statusMessage?: string): string;
/**
* Make sure the status code is a valid HTTP status code.
*/
declare function sanitizeStatusCode(statusCode?: string | number, defaultStatusCode?: number): number;
interface CacheConditions {
modifiedTime?: string | Date;
maxAge?: number;
etag?: string;
cacheControls?: string[];
}
/**
* Check request caching headers (`If-None-Match`, `If-Modified-Since`) and add caching headers (Last-Modified, ETag, Cache-Control).
*
* Note: `public` is added by default, but never alongside a caller-supplied `private`/`no-store` directive, so passing `cacheControls: ["private"]` no longer produces a contradictory `public, private`.
* @returns `true` when cache headers are matching. When `true` is returned, no response should be sent anymore
*/
declare function handleCacheHeaders(event: H3Event, opts: CacheConditions): boolean;
interface ResolveDotSegmentsOptions {
/**
* Also decode percent-encoded path separators (`%2f`, `%5c`) into real `/`
* segment boundaries before resolving `.`/`..`.
*
* `event.url.pathname` never decodes `%2f` on its own, because doing so
* would change how many segments a path has and therefore which route
* matches — a correctness concern for dispatch, not just a security one
* (e.g. `/files/:id` may rely on `%2F` to keep an id with a literal slash
* as one opaque segment). So never use the result for routing/dispatch.
*
* Enable this for any out-of-band scope/security check whose result is
* later handed to something that collapses `%2f` back to `/` on its own —
* which is the common case, not an exotic one: an ordinary reverse proxy
* (e.g. nginx with a trailing-slash `proxy_pass`) decodes `%2f`→`/` on every
* request, so an encoded separator that dodges a narrower rule at match time
* then escapes it downstream. If a scope check feeds a proxy or redirect
* target, you almost certainly want this on.
*
* Decoding is pessimistic but bounded: it collapses a separator nested as
* repeated whole `%25` prefixes (`%252f`, `%25252f`, ...) at any depth, so a
* downstream that keeps `%25`-re-encoding and decoding cannot smuggle one
* past. It does NOT catch a separator whose own hex digits are themselves
* percent-encoded (`%25%32%66` → `%2f` → `/` after two decodes) — and that
* form can appear even in an already-once-decoded pathname (from wire
* `%2525%2532%2566`), so once-decoded input is not by itself sufficient.
* Treat this as covering the common `%25`-nesting case, not as an absolute
* guarantee against every multi-decode chain. Other escapes (e.g. `%20`) are
* never decoded.
*
* @default false
*/
decodeSlashes?: boolean;
}
/**
* Resolve `.` and `..` segments in a path, without ever escaping above the
* root `/`. The result is always an absolute path with a single leading `/`,
* so it can never be protocol-relative (`//host`).
*
* Also decodes percent-encoded dot segments at any `%25`-nesting depth
* (`%2e`, `%252e`, ...) and normalizes `\` to `/`, so encoded or
* backslash-based traversal (e.g. `%2e%2e/`, `..\..\`) is caught the same
* way as a literal `../`.
*
* `%2f`/`%5c` (encoded path separators) are left untouched by default — see
* {@link ResolveDotSegmentsOptions.decodeSlashes}.
*
* Only `.`/`..` resolution and the decodes above alter the string; every other
* percent-encoding (`%20`, non-ASCII, `%3A`, and any `%2e` not forming a whole
* segment) is left intact, so the result stays in the same representation as an
* un-decoded `event.url.pathname` and matches routes/rules consistently.
* Interior empty segments are preserved (`/a//b` stays `/a//b`, per WHATWG URL
* normalization) — only the leading slash is guaranteed single, so a consumer
* doing exact prefix matching should normalize its allowlist the same way.
*/
declare function resolveDotSegments(path: string, opts?: ResolveDotSegmentsOptions): string;
interface StaticAssetMeta {
type?: string;
etag?: string;
mtime?: number | string | Date;
path?: string;
size?: number;
encoding?: string;
}
interface ServeStaticOptions {
/**
* This function should resolve asset meta
*/
getMeta: (id: string) => StaticAssetMeta | undefined | Promise<StaticAssetMeta | undefined>;
/**
* This function should resolve asset content
*/
getContents: (id: string) => BodyInit | null | undefined | Promise<BodyInit | null | undefined>;
/**
* Headers to set on the response
*/
headers?: HeadersInit;
/**
* Map of supported encodings (compressions) and their file extensions.
*
* Each extension will be appended to the asset path to find the compressed version of the asset.
*
* @example { gzip: ".gz", br: ".br" }
*/
encodings?: Record<string, string>;
/**
* Default index file to serve when the path is a directory
*
* @default ["/index.html"]
*/
indexNames?: string[];
/**
* When set to true, the function will not throw 404 error when the asset meta is not found or meta validation failed
*/
fallthrough?: boolean;
/**
* Custom MIME type resolver function
* @param ext - File extension including dot (e.g., ".css", ".js")
*/
getType?: (ext: string) => string | undefined;
}
/**
* Dynamically serve static assets based on the request path.
*/
declare function serveStatic(event: H3Event, options: ServeStaticOptions): Promise<HTTPResponse | undefined>;
/**
* Returns a new event handler that removes the base url of the event before calling the original handler.
*
* @example
* const api = new H3()
* .get("/", () => "Hello API!");
* const app = new H3();
* .use("/api/**", withBase("/api", api.handler));
*
* @param base The base path to prefix.
* @param handler The event handler to use with the adapted path.
*/
declare function withBase(base: string, input: HTTPHandler): EventHandler;
/**
* Check if the origin is allowed.
*/
declare function isCorsOriginAllowed(origin: string | null | undefined, options: CorsOptions): boolean;
interface CorsOptions {
/**
* This determines the value of the "access-control-allow-origin" response header.
* If "*", it can be used to allow all origins.
* If an array of strings or regular expressions, it can be used with origin matching.
* If a custom function, it's used to validate the origin. It takes the origin as an argument and returns `true` if allowed.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
* @default "*"
*/
origin?: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean);
/**
* This determines the value of the "access-control-allow-methods" response header of a preflight request.
*
* The default `"*"` permits any method (including non-safelisted ones like `QUERY`).
* When using an explicit allowlist, remember that `QUERY` is **not** a CORS-safelisted
* method, so browsers preflight it — include `"QUERY"` in the array to allow it.
*
* When `credentials` is enabled, browsers treat `"*"` as a literal method name — in that
* case the requested method is reflected back instead of sending a literal `*`.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
* @default "*"
* @example ["GET", "HEAD", "PUT", "POST", "QUERY"]
*/
methods?: "*" | string[];
/**
* This determines the value of the "access-control-allow-headers" response header of a preflight request.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
* @default "*"
*/
allowHeaders?: "*" | string[];
/**
* This determines the value of the "access-control-expose-headers" response header.
*
* When `credentials` is enabled, browsers treat `"*"` as a literal header name — in that
* case the header is omitted; list the headers explicitly to expose them.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
* @default "*"
*/
exposeHeaders?: "*" | string[];
/**
* This determines the value of the "access-control-allow-credentials" response header.
* When request with credentials, the options that `origin`, `methods`, `exposeHeaders` and `allowHeaders` should not be set "*".
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
* @see https://fetch.spec.wh