UNPKG

h3

Version:

Minimal H(TTP) framework built for high performance and portability.

1,154 lines (1,153 loc) 82.4 kB
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 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">); /** * Status of the response, or `undefined` when unset. * * Unset means "inherit": the status staged on `event.res.status` is used, falling back to `200`. * Defaulting to `200` here instead would make an untouched `HTTPResponse` indistinguishable from * one explicitly built with `{ status: 200 }`, and always win over `event.res`. */ get status(): number | undefined; /** Status text of the response, or `undefined` when unset. See {@link HTTPResponse.status}. */ get statusText(): string | undefined; 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. * * **Security:** The returned host reflects the client-supplied `Host` (or * `X-Forwarded-Host`) header and can be spoofed. Do not trust it for security * decisions (CSRF/origin checks, cache keys, generating absolute links sent to * other users) unless the `Host` value is pinned or validated upstream (e.g. an * allow-list of expected hosts, or a reverse proxy that overwrites it). * * @example * app.get("/", (event) => { * const host = getRequestHost(event); // "example.com" * }); */ declare function getRequestHost(event: HTTPEvent, opts?: { xForwardedHost?: boolean; }): string; /** * Get the request protocol. * * If `xForwardedProto` is `true`, it will use the `x-forwarded-proto` header if it exists. When the header contains a comma-separated list of protocols, the first entry is used. * * Note: This header is opt-in (default `false`) since it can be spoofed by clients. Only enable it when your application runs behind a trusted reverse proxy or CDN that sets this header. This default was changed to match `getRequestHost` (`xForwardedHost`) and `getRequestIP` (`xForwardedFor`). * * 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 `true`, it will use the `x-forwarded-proto` header if it exists. * * **Security:** The `.origin` and `.host` of the returned URL are derived from the * client-supplied `Host` (or `X-Forwarded-Host`) header and can be spoofed. Do not * trust them for security decisions (CSRF/origin checks, cache keys, generating * absolute links sent to other users) unless the `Host` value is pinned or * validated upstream (e.g. an allow-list of expected hosts, or a reverse proxy * that overwrites it). The `.pathname` and `.search` are not derived from the * spoofable host, but remain untrusted client input — validate or encode them for * their eventual sink (e.g. filesystem lookups, HTML output, downstream queries). * * @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; type DisposeCallback = (reason?: unknown) => unknown; /** * Register a callback that runs once the event is fully over: the response body finished streaming, the client disconnected, or the body errored — on every runtime, not just Node.js. * * The callback receives `undefined` on normal completion, or the cancel/abort reason otherwise. Callbacks run in registration order after the global `onResponse` hook; sync throws and async rejections are absorbed (reported via `console.error` unless the app is configured with `silent`), and pending async callbacks are passed to `waitUntil`. * * Registering after disposal invokes the callback immediately. Registration is only guaranteed to observe the end of the event when made during request handling (handler, middleware, or `onResponse`). * * Note: this signals _"h3 is done with this event"_, not _"the client received the response"_ — for non-streaming bodies on non-Node.js runtimes it fires when the response is handed to the runtime. To react to a client disconnect _while still producing_ the response (for example to abort an upstream fetch), use `event.req.signal` instead. * * @example * app.get("/sse", (event) => { * const interval = setInterval(() => {}, 1000); * onDispose(event, () => clearInterval(interval)); * // ... return a streaming response * }); */ declare function onDispose(event: H3Event, cb: DisposeCallback): void; /** * 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. * * **Security:** If `location` derives from user input (query params, form fields, * headers, etc.), validate it against an allow-list of permitted destinations * before redirecting. Passing user-controlled values through unchecked creates an * open redirect vulnerability. Prefer `redirectBack` for "return to previous page" * flows, which only honors same-origin referers. * * @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. * * The first chunk is awaited before the response is created, so status and headers staged while * producing it (`event.res.status`, `event.res.headers`) are still applied. Everything set after * the first chunk is ignored — headers are already on the wire by then. (Returning a raw * `ReadableStream` gives no such window: its response is created before the stream is read.) * * @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>; }): Promise<HTTPResponse>; /** * Respond with HTML content. * * When used as a **tagged template**, interpolated values are automatically * HTML-escaped (`& < > " '`) to help prevent XSS. Wrap a value with {@link raw} * to opt out of escaping for trusted markup. * * When called with a **plain string**, the whole string is HTML-escaped and * rendered as text. If escaping changes the input, a warning is logged — use * the tagged template for dynamic values, or pass trusted markup with * {@link raw}: `html(raw(markup))`. * * Escaping protects values in element content and inside quoted attribute * values only. It cannot make unquoted attributes, URL attributes (e.g. * `href` with a `javascript:` URL) or `<script>`/`<style>` contents safe — * validate such values separately. * * @example * // Tagged template (interpolations are escaped): * app.get("/", () => html`<h1>Hello, ${name}!</h1>`); * * @example * // Trusted markup (used as-is, not escaped): * app.get("/", () => html(raw("<h1>Hello, World!</h1>"))); * * @example * // Opt out of escaping for a trusted interpolation: * app.get("/", () => html`<div>${raw(trustedMarkup)}</div>`); */ declare function html(strings: TemplateStringsArray, ...values: unknown[]): HTTPResponse; declare function html(markup: string | RawHTML): HTTPResponse; /** * Mark a string as trusted, pre-escaped HTML so it is used by the * {@link html} util **without** being escaped. * * Only use this for markup you fully control — passing user input to `raw` * re-introduces XSS risk. * * @example * // `heading` is trusted markup; `userName` is escaped automatically. * app.get("/", () => html`<div>${raw(heading)}<span>${userName}</span></div>`); * * @example * // Send a trusted markup string as-is: * app.get("/", () => html(raw("<h1>Hello, World!</h1>"))); */ declare function raw(value: string): RawHTML; /** Trusted raw HTML wrapper produced by {@link raw}. */ interface RawHTML { readonly value: string; } /** * 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: `*` / `*&#47;*` 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 limits the request body size to the specified limit. * * The limit is enforced as the body is read (see {@link assertBodySize}), so an * oversized body surfaces as a `413` Request Entity Too Large error when the * handler consumes it (an honest oversized `Content-Length` is still rejected * up-front). A body the handler never reads is not counted. If you need custom * handling, use `assertBodySize` directly. * * @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 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. * * Only the "soft" drops (`host`, `accept-encoding`, `expect`) can be * overridden this way. It can **never** force-forward a true hop-by-hop framing header * (`connection`, `keep-alive`, `transfer-encoding`, `te`, `trailer`, * `upgrade`, `proxy-authorization`, `proxy-connection`) or a field the * incoming `Connection` header nominates — forwarding those could desync * request framing or leak the inbound proxy's credentials upstream, so they * are always dropped. */ 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. * * **Credential forwarding:** the incoming request's `Cookie` and `Authorization` * headers are forwarded to the `target` verbatim. This is the correct behavior * for a same-trust reverse proxy, but leaks the client's credentials to any * upstream you do not fully trust. When proxying to a not-fully-trusted upstream, * strip them with `filterHeaders: ["cookie", "authorization"]`. (This differs * from `fetchWithEvent`, which never forwards the event's headers to an external * URL.) * * @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). * * **Credential forwarding:** `proxy` does not forward the incoming request's * headers automatically — only headers the caller explicitly passes via * `opts.headers` (or `fetchOptions.headers`) are sent, verbatim. Do not pass the * client's `Cookie` or `Authorization` headers through to an upstream you do not * fully trust. Note that `opts.filterHeaders` has no effect here — it is only * applied by `proxyRequest` (which does forward the incoming headers and offers * `filterHeaders: ["cookie", "authorization"]` as the mitigation). */ 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?: ReadBodyOptions & { 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?: ReadBodyOptions & { onError?: () => ErrorDetails; }): Promise<OutputT>; /** * Asserts that the request body size is within the specified limit. * * The limit is enforced **as the body is read**, not by pre-buffering: the * request is wrapped by srvx's `limitRequestBody`, which counts bytes as they * flow and aborts with a `413` {@link HTTPError} the moment the running total * exceeds `limit` (the error is injected via `createError`). This preserves the * byte-accurate guarantee (a lying-small `Content-Length` is still caught * mid-stream) without holding the body in memory or blocking streaming handlers. * * An honest `Content-Length` that already exceeds the limit is rejected up-front * with a `413`, and a request carrying both `Content-Length` and * `Transfer-Encoding` is rejected with a `400` (request smuggling, RFC 7230). * * Because enforcement is tied to consumption, an overflow on a chunked / * unknown-length body surfaces when the handler reads the body rather than as a * pre-handler `413`, and a body the handler never reads is never counted. * * @example * app.post("/", async (event) => { * 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): 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; /** * Options for the {@link EventStream} constructor. * * Currently empty — reserved for future configuration. */ interface EventStreamOptions {} /** * 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; } /** * 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) * * Extends {@link HTTPResponse} so it can be returned directly from a handler * (`return eventStream`) — `toResponse` already renders any `HTTPResponse` as * the response, streaming the readable side with the SSE headers below. * * @example * * ```ts * import { EventStream } from "h3"; * * app.get("/sse", (event) => { * const eventStream = new EventStream(event); * * // Send a message every second * const interval = setInterval(async () => { * await eventStream.push("Hello world"); * }, 1000); * * // cleanup the interval when the connection is terminated * eventStream.onClosed(() => clearInterval(interval)); * * return eventStream; * }); * ``` */ declare class EventStream extends HTTPResponse { private readonly _event; private readonly _transformStream; private readonly _writer; private readonly _encoder; private readonly _closeCallbacks; private _writerIsClosed; private _paused; private _unsentData; private _disposed; 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 stream is closed, either by calling the * `close()` method or when the client disconnects. */ onClosed(cb: () => any): void; /** * Return the readable side of the stream, staging the SSE headers on the event. * * @deprecated Return the stream itself instead (`return eventStream`) — it * carries the same headers via {@link HTTPResponse}. Kept for compatibility * with the `return eventStream.send()` pattern. */ send(): Promise<BodyInit>; } /** * 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 contradicto