Security

H3 security utilities.

#Authentication

#basicAuth(opts)

Create a basic authentication middleware.

Example:

import { H3, serve, basicAuth } from "h3";
const auth = basicAuth({ password: "test" });
app.get("/", (event) => `Hello ${event.context.basicAuth?.username}!`, [auth]);
serve(app, { port: 3000 });

#requireBasicAuth(event, opts)

Apply basic authentication for current request.

Example:

import { defineHandler, requireBasicAuth } from "h3";
export default defineHandler(async (event) => {
  await requireBasicAuth(event, { password: "test" });
  return `Hello, ${event.context.basicAuth.username}!`;
});

#Session

#clearSession(event, config)

Clear the session data for the current request.

#getSession(event, config)

Get the session for the current request.

A request without a session gets a new one initialized in memory only — no Set-Cookie is issued until something is stored with {@link updateSession}, so reading the session (an auth check, for example) does not start one for anonymous visitors. Its id is therefore only stable across requests once the session has been written; use {@link useSession} to start one eagerly.

#sealSession(event, config)

Encrypt and sign the session data for the current request.

#unsealSession(_event, config, sealed)

Decrypt and verify the session data for the current request.

#updateSession(event, config, update?)

Update the session data for the current request.

#useSession(event, config)

Create a session manager for the current request.

Starts a session if the request does not carry one, persisting it so its id is stable across requests. Use {@link getSession} to read a session without starting one.

#Fingerprint

#getRequestFingerprint(event, opts)

Get a unique fingerprint for the incoming request.

#CORS

#appendCorsHeaders(event, options)

Append CORS headers to the response.

#appendCorsPreflightHeaders(event, options)

Append CORS preflight headers to the response.

#handleCors(event, options)

Handle CORS for the incoming request.

If the incoming request is a CORS preflight request, it will append the CORS preflight headers and send a 204 response.

If return value is not false, the request is handled and no further action is needed.

Example:

const app = new H3();
app.all("/", async (event) => {
  const corsRes = handleCors(event, {
    origin: "*",
    preflight: {
      statusCode: 204,
    },
    methods: "*",
  });
  if (corsRes !== false) {
    return corsRes;
  }
  // Your code here
});

#isCorsOriginAllowed(origin, options)

Check if the origin is allowed.

#isPreflightRequest(event)

Check if the incoming request is a CORS preflight request.

#Path

#isCanonicalPath(path, opts?)

Whether path is already canonical under opts — i.e. {@link resolveDotSegments} would return it unchanged. Exact in both directions: true if and only if resolveDotSegments(path, opts) === path.

This is the resolver's own fast-path guard, exported so a caller that canonicalizes on a hot path (per-request scope or rule matching) can skip the call — and any work derived from it — without keeping its own copy of what the resolver decodes. Such a copy goes stale silently, and a missed canonicalization in a scope check is a bypass, not a perf bug.

Pass the same options as the later {@link resolveDotSegments} call, or stricter ones: decodeSlashes/mergeSlashes only add triggers, so true with both enabled implies true in every mode. Checking one mode and resolving in another voids the guarantee.

Takes a bare pathname. Like the resolver, it has no notion of a query or hash and scans one as if it were path, so /a?next=/../b is reported non-canonical (and would resolve to /b).

#normalizeRoute(route)

Normalize a route pattern into the canonical form h3 registers it under — the same shape as the event.url.pathname it will be matched against.

app.on(), app.use(route, …), app.mount() and removeRoute() all apply this to the pattern they receive. Use it when registering patterns into a router of your own (e.g. a build-time compiled rou3 router) that is then matched against h3's event.url.pathname, so both sides agree on the string — a pattern that normalized differently could leave a route reachable while a guard registered with the same source string matches nothing.

A leading / is added if missing (about/about), characters a request pathname always carries percent-encoded are encoded (/café/**/caf%C3%A9/**), needless escapes are decoded the way h3 decodes them in the request pathname (/%40handle/@handle; %2F and %25 stay encoded), and ./.. segments are resolved (/a/b/../c/a/c). rou3 pattern syntax (?, {, }, ^, \) is left verbatim — spell one percent-encoded to match it literally.

Idempotent. Throws on an absolute URL (http://…): a route pattern is a pathname, never a URL.

Example:

normalizeRoute("/について/**"); // "/%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/**"

#resolveDotSegments(path, opts?)

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 event.url.pathname and matches routes/rules consistently. A trailing ./.. resolves to a directory and keeps its trailing slash (/a/b/.. -> /a/, /a/. -> /a/), per RFC 3986 §5.2.4 and matching what a WHATWG/nginx downstream resolves — so a scope check sees the directory form, not its file-form sibling. Interior empty segments are preserved (/a//b stays /a//b) — like WHATWG, this never merges slashes, so empty segments survive rather than collapsing. The one exception is a leading run: it is always clamped to a single / (WHATWG would keep //host), so only the leading slash is guaranteed single and a consumer doing exact prefix matching should normalize its allowlist the same way. To collapse interior runs too (the reading a slash-merging downstream resolves), see {@link ResolveDotSegmentsOptions.mergeSlashes}.

#Route params

Route params reach your handler in the form they had in the URL path — percent-encoded. getRouterParams(event, { decode: true }) (and getValidatedRouterParams with the same option) applies one decode pass, not a full normalization:

  • Encoded path separators (%2f, %5c, at any %25-nesting depth: %252f, %25252f, ...) are never decoded. A raw / or \ can never appear in a param that the router matched as one segment, so a param cannot silently gain a path boundary that routing and middleware never saw.
  • Every other escape decodes exactly one level. Because %25 is itself an escape, %25XX decodes to the literal text %XX — so the result can still contain percent-escapes.
app.get("/files/**:rest", (event) => {
  // GET /files/%252e%252e/x
  getRouterParams(event); // { rest: "%252e%252e/x" }
  getRouterParams(event, { decode: true }); // { rest: "%2e%2e/x" }

  // GET /files/%2500
  getRouterParams(event, { decode: true }); // { rest: "%00" }

  // GET /files/a%252fb  — separators stay encoded at every depth
  getRouterParams(event, { decode: true }); // { rest: "a%252fb" }
});

Important

Do not decode the returned value again. A second decodeURIComponent turns %2e%2e/x into ../x and %00 into a NUL byte — traversal and control characters that were not visible to routing or to any pathname-based middleware. Validate the value as returned, and if it will be used as a filesystem or upstream path, resolve it with resolveDotSegments rather than by decoding further.

H3  Universal, Tiny, and Fast Servers.