@depup/h3 2.0.1-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/bin/h3.mjs +36 -0
  4. package/changes.json +5 -0
  5. package/dist/THIRD-PARTY-LICENSES.md +70 -0
  6. package/dist/_entries/bun.d.mts +6 -0
  7. package/dist/_entries/bun.mjs +16 -0
  8. package/dist/_entries/cloudflare.d.mts +6 -0
  9. package/dist/_entries/cloudflare.mjs +16 -0
  10. package/dist/_entries/deno.d.mts +6 -0
  11. package/dist/_entries/deno.mjs +16 -0
  12. package/dist/_entries/generic.d.mts +6 -0
  13. package/dist/_entries/generic.mjs +16 -0
  14. package/dist/_entries/node.d.mts +10 -0
  15. package/dist/_entries/node.mjs +19 -0
  16. package/dist/_entries/service-worker.d.mts +6 -0
  17. package/dist/_entries/service-worker.mjs +16 -0
  18. package/dist/_utils.mjs +240 -0
  19. package/dist/cache.mjs +599 -0
  20. package/dist/cache2.mjs +50 -0
  21. package/dist/cors.mjs +292 -0
  22. package/dist/docs/0.guide/0.index/index.md +117 -0
  23. package/dist/docs/0.guide/1.basics/0.lifecycle.md +68 -0
  24. package/dist/docs/0.guide/1.basics/1.routing.md +167 -0
  25. package/dist/docs/0.guide/1.basics/2.middleware.md +97 -0
  26. package/dist/docs/0.guide/1.basics/3.handler.md +165 -0
  27. package/dist/docs/0.guide/1.basics/4.response.md +171 -0
  28. package/dist/docs/0.guide/1.basics/5.error.md +117 -0
  29. package/dist/docs/0.guide/1.basics/6.nested-apps.md +57 -0
  30. package/dist/docs/0.guide/2.rules.md +698 -0
  31. package/dist/docs/0.guide/3.api/0.h3.md +144 -0
  32. package/dist/docs/0.guide/3.api/1.h3event.md +160 -0
  33. package/dist/docs/0.guide/4.advanced/0.plugins.md +50 -0
  34. package/dist/docs/0.guide/4.advanced/1.websocket.md +176 -0
  35. package/dist/docs/0.guide/4.advanced/2.nightly.md +13 -0
  36. package/dist/docs/1.utils/0.index/index.md +46 -0
  37. package/dist/docs/1.utils/1.request.md +447 -0
  38. package/dist/docs/1.utils/2.response.md +172 -0
  39. package/dist/docs/1.utils/3.cookie.md +33 -0
  40. package/dist/docs/1.utils/4.security.md +175 -0
  41. package/dist/docs/1.utils/5.proxy.md +57 -0
  42. package/dist/docs/1.utils/6.mcp.md +75 -0
  43. package/dist/docs/1.utils/7.more.md +117 -0
  44. package/dist/docs/1.utils/8.community.md +48 -0
  45. package/dist/docs/2.examples/0.index/index.md +17 -0
  46. package/dist/docs/2.examples/1.handle-cookie.md +67 -0
  47. package/dist/docs/2.examples/2.handle-query.md +76 -0
  48. package/dist/docs/2.examples/3.handle-session.md +210 -0
  49. package/dist/docs/2.examples/4.serve-static-assets.md +66 -0
  50. package/dist/docs/2.examples/5.stream-response.md +76 -0
  51. package/dist/docs/2.examples/6.validate-data.md +193 -0
  52. package/dist/docs/3.migration/0.index/index.md +204 -0
  53. package/dist/docs/README.md +37 -0
  54. package/dist/h3.d.mts +1669 -0
  55. package/dist/h3.mjs +1809 -0
  56. package/dist/index.d.mts +1634 -0
  57. package/dist/match.d.mts +123 -0
  58. package/dist/middleware.mjs +123 -0
  59. package/dist/normalize.mjs +645 -0
  60. package/dist/path.mjs +42 -0
  61. package/dist/proxy.mjs +254 -0
  62. package/dist/response.mjs +465 -0
  63. package/dist/rules/cache.d.mts +29 -0
  64. package/dist/rules/cache.mjs +163 -0
  65. package/dist/rules/compiler.d.mts +94 -0
  66. package/dist/rules/compiler.mjs +173 -0
  67. package/dist/rules/index.d.mts +77 -0
  68. package/dist/rules/index.mjs +34 -0
  69. package/dist/rules/proxy.d.mts +3 -0
  70. package/dist/rules/proxy.mjs +14 -0
  71. package/dist/tracing.d.mts +33 -0
  72. package/dist/tracing.mjs +89 -0
  73. package/package.json +148 -0
package/dist/cors.mjs ADDED
@@ -0,0 +1,292 @@
1
+ import { HTTPResponse, onDispose as onDispose$1 } from "./response.mjs";
2
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
3
+ const textDecoder = /* @__PURE__ */ new TextDecoder();
4
+ const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5
+ function base64Encode(data) {
6
+ const buff = validateBinaryLike(data);
7
+ if (globalThis.Buffer) return globalThis.Buffer.from(buff).toString("base64url");
8
+ let result = "";
9
+ let i;
10
+ const len = buff.length;
11
+ for (i = 2; i < len; i += 3) result += base64Chars[buff[i - 2] >> 2] + base64Chars[(buff[i - 2] & 3) << 4 | buff[i - 1] >> 4] + base64Chars[(buff[i - 1] & 15) << 2 | buff[i] >> 6] + base64Chars[buff[i] & 63];
12
+ if (i === len + 1) result += base64Chars[buff[i - 2] >> 2] + base64Chars[(buff[i - 2] & 3) << 4];
13
+ if (i === len) result += base64Chars[buff[i - 2] >> 2] + base64Chars[(buff[i - 2] & 3) << 4 | buff[i - 1] >> 4] + base64Chars[(buff[i - 1] & 15) << 2];
14
+ return result;
15
+ }
16
+ function base64Decode(b64Url) {
17
+ if (globalThis.Buffer) return new Uint8Array(globalThis.Buffer.from(b64Url, "base64url"));
18
+ const b64 = b64Url.replace(/-/g, "+").replace(/_/g, "/");
19
+ const binString = atob(b64);
20
+ const size = binString.length;
21
+ const bytes = new Uint8Array(size);
22
+ for (let i = 0; i < size; i++) bytes[i] = binString.charCodeAt(i);
23
+ return bytes;
24
+ }
25
+ function validateBinaryLike(source) {
26
+ if (typeof source === "string") return textEncoder.encode(source);
27
+ else if (source instanceof Uint8Array) return source;
28
+ else if (source instanceof ArrayBuffer) return new Uint8Array(source);
29
+ throw new TypeError(`The input must be a Uint8Array, a string, or an ArrayBuffer.`);
30
+ }
31
+ function serializeIterableValue(value) {
32
+ switch (typeof value) {
33
+ case "string": return textEncoder.encode(value);
34
+ case "boolean":
35
+ case "number":
36
+ case "bigint":
37
+ case "symbol": return textEncoder.encode(value.toString());
38
+ case "object":
39
+ if (value instanceof Uint8Array) return value;
40
+ return textEncoder.encode(JSON.stringify(value));
41
+ }
42
+ return /* @__PURE__ */ new Uint8Array();
43
+ }
44
+ function coerceIterable(iterable) {
45
+ if (typeof iterable === "function") iterable = iterable();
46
+ if (Symbol.iterator in iterable) return iterable[Symbol.iterator]();
47
+ if (Symbol.asyncIterator in iterable) return iterable[Symbol.asyncIterator]();
48
+ return iterable;
49
+ }
50
+ function onDispose(event, cb) {
51
+ onDispose$1(event, cb);
52
+ }
53
+ function noContent(status = 204) {
54
+ return new HTTPResponse(null, {
55
+ status,
56
+ statusText: "No Content"
57
+ });
58
+ }
59
+ function redirect(location, status = 302, statusText) {
60
+ const body = `<html><head><meta http-equiv="refresh" content="0; url=${escapeHtml(location)}" /></head></html>`;
61
+ return new HTTPResponse(body, {
62
+ status,
63
+ statusText: statusText || (status === 301 ? "Moved Permanently" : "Found"),
64
+ headers: {
65
+ "content-type": "text/html; charset=utf-8",
66
+ location
67
+ }
68
+ });
69
+ }
70
+ function redirectBack(event, opts = {}) {
71
+ const referer = event.req.headers.get("referer");
72
+ let location = opts.fallback ?? "/";
73
+ if (referer && URL.canParse(referer)) {
74
+ const refererURL = new URL(referer);
75
+ if (refererURL.origin === event.url.origin) {
76
+ let pathname = refererURL.pathname;
77
+ if (pathname.startsWith("//")) pathname = "/" + pathname.replace(/^\/+/, "");
78
+ location = pathname + (opts.allowQuery ? refererURL.search : "");
79
+ }
80
+ }
81
+ return redirect(location, opts.status);
82
+ }
83
+ function writeEarlyHints(event, hints) {
84
+ const linkValues = [];
85
+ for (const [name, value] of Object.entries(hints)) if (name.toLowerCase() === "link") {
86
+ for (const v of Array.isArray(value) ? value : [value]) if (v) linkValues.push(v);
87
+ }
88
+ if (event.runtime?.node?.res?.writeEarlyHints) {
89
+ if (linkValues.length === 0) return Promise.resolve();
90
+ const normalizedHints = { link: linkValues };
91
+ for (const [name, value] of Object.entries(hints)) if (name.toLowerCase() !== "link") normalizedHints[name] = value;
92
+ return new Promise((resolve) => {
93
+ event.runtime?.node?.res?.writeEarlyHints(normalizedHints, () => resolve());
94
+ });
95
+ }
96
+ for (const v of linkValues) event.res.headers.append("link", v);
97
+ }
98
+ async function iterable(iterable, options) {
99
+ const serializer = options?.serializer ?? serializeIterableValue;
100
+ const iterator = coerceIterable(iterable);
101
+ let first = await iterator.next();
102
+ return new HTTPResponse(new ReadableStream({
103
+ async pull(controller) {
104
+ const { value, done } = first ?? await iterator.next();
105
+ first = void 0;
106
+ if (value !== void 0) {
107
+ const chunk = serializer(value);
108
+ if (chunk !== void 0) controller.enqueue(chunk);
109
+ }
110
+ if (done) controller.close();
111
+ },
112
+ cancel() {
113
+ iterator.return?.();
114
+ }
115
+ }));
116
+ }
117
+ function html(first, ...values) {
118
+ let body;
119
+ if (typeof first === "string") {
120
+ body = escapeHtml(first);
121
+ if (body !== first && html._isWarned !== true) {
122
+ html._isWarned = true;
123
+ console.warn("[h3] `html()` received a plain string containing HTML characters and escaped it. Use the html`` tagged template for dynamic values, or wrap trusted markup with `raw()`.");
124
+ }
125
+ } else if (isRawHTML(first)) body = first.value;
126
+ else body = first.reduce((out, str, i) => {
127
+ const value = values[i];
128
+ const rendered = value == null ? "" : isRawHTML(value) ? value.value : escapeHtml(String(value));
129
+ return out + str + rendered;
130
+ }, "");
131
+ return new HTTPResponse(body, { headers: { "content-type": "text/html; charset=utf-8" } });
132
+ }
133
+ function raw(value) {
134
+ return {
135
+ [kRawHTML]: true,
136
+ value
137
+ };
138
+ }
139
+ const kRawHTML = /* @__PURE__ */ Symbol("h3.rawHTML");
140
+ function isRawHTML(value) {
141
+ return typeof value === "object" && value !== null && value[kRawHTML] === true;
142
+ }
143
+ const HTML_ESCAPES = {
144
+ "&": "&amp;",
145
+ "\"": "&quot;",
146
+ "'": "&#39;",
147
+ "<": "&lt;",
148
+ ">": "&gt;"
149
+ };
150
+ function escapeHtml(str) {
151
+ return str.replace(/[&"'<>]/g, (c) => HTML_ESCAPES[c]);
152
+ }
153
+ function resolveCorsOptions(options = {}) {
154
+ const defaultOptions = {
155
+ origin: "*",
156
+ methods: "*",
157
+ allowHeaders: "*",
158
+ exposeHeaders: "*",
159
+ credentials: false,
160
+ maxAge: false,
161
+ preflight: { statusCode: 204 }
162
+ };
163
+ const resolved = {
164
+ ...defaultOptions,
165
+ ...options,
166
+ preflight: {
167
+ ...defaultOptions.preflight,
168
+ ...options.preflight
169
+ }
170
+ };
171
+ if (resolved.credentials && resolved.origin === "*") warnOnce("[h3] CORS: `credentials: true` with wildcard origin is not allowed. Browsers will reject the response.");
172
+ if (resolved.credentials && (resolved.origin === "null" || Array.isArray(resolved.origin) && resolved.origin.includes("null"))) warnOnce("[h3] CORS: `credentials: true` with a `\"null\"` origin is dangerous. Any sandboxed iframe, `data:`/`file:` document, or opaque origin sends `Origin: null`, so credentials would be shared across untrusted contexts.");
173
+ if (resolved.credentials && resolved.exposeHeaders === "*") warnOnce("[h3] CORS: `credentials: true` with wildcard `exposeHeaders` has no effect. Browsers treat `*` literally on credentialed requests — list the headers explicitly.");
174
+ return resolved;
175
+ }
176
+ function isCorsOriginAllowed(origin, options) {
177
+ const { origin: originOption } = options;
178
+ if (!origin) return false;
179
+ if (!originOption || originOption === "*") return true;
180
+ if (typeof originOption === "function") return originOption(origin);
181
+ if (Array.isArray(originOption)) return originOption.some((_origin) => {
182
+ if (_origin instanceof RegExp) return _origin.test(origin);
183
+ return origin === _origin;
184
+ });
185
+ return originOption === origin;
186
+ }
187
+ function createOriginHeaders(event, options) {
188
+ const { origin: originOption } = options;
189
+ const origin = event.req.headers.get("origin");
190
+ if (!originOption || originOption === "*") return { "access-control-allow-origin": "*" };
191
+ if (isCorsOriginAllowed(origin, options)) return {
192
+ "access-control-allow-origin": origin,
193
+ vary: "origin"
194
+ };
195
+ return { vary: "origin" };
196
+ }
197
+ function createMethodsHeaders(event, options) {
198
+ const { methods, credentials } = options;
199
+ if (!methods) return {};
200
+ if (methods === "*") {
201
+ if (credentials) {
202
+ const requestMethod = event.req.headers.get("access-control-request-method");
203
+ return requestMethod ? {
204
+ "access-control-allow-methods": requestMethod,
205
+ vary: "access-control-request-method"
206
+ } : {};
207
+ }
208
+ return { "access-control-allow-methods": "*" };
209
+ }
210
+ return methods.length > 0 ? { "access-control-allow-methods": methods.join(",") } : {};
211
+ }
212
+ function createCredentialsHeaders(options) {
213
+ const { credentials } = options;
214
+ if (credentials) return { "access-control-allow-credentials": "true" };
215
+ return {};
216
+ }
217
+ function createAllowHeaderHeaders(event, options) {
218
+ const { allowHeaders } = options;
219
+ if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) {
220
+ const header = event.req.headers.get("access-control-request-headers");
221
+ return header ? {
222
+ "access-control-allow-headers": header,
223
+ vary: "access-control-request-headers"
224
+ } : { vary: "access-control-request-headers" };
225
+ }
226
+ return {
227
+ "access-control-allow-headers": allowHeaders.join(","),
228
+ vary: "access-control-request-headers"
229
+ };
230
+ }
231
+ function createExposeHeaders(options) {
232
+ const { exposeHeaders, credentials } = options;
233
+ if (!exposeHeaders) return {};
234
+ if (exposeHeaders === "*") return credentials ? {} : { "access-control-expose-headers": exposeHeaders };
235
+ return { "access-control-expose-headers": exposeHeaders.join(",") };
236
+ }
237
+ function createMaxAgeHeader(options) {
238
+ const { maxAge } = options;
239
+ if (maxAge) return { "access-control-max-age": maxAge };
240
+ return {};
241
+ }
242
+ let warnedMessages;
243
+ function warnOnce(message) {
244
+ warnedMessages ??= /* @__PURE__ */ new Set();
245
+ if (warnedMessages.has(message)) return;
246
+ warnedMessages.add(message);
247
+ console.warn(message);
248
+ }
249
+ function isPreflightRequest(event) {
250
+ const origin = event.req.headers.get("origin");
251
+ const accessControlRequestMethod = event.req.headers.get("access-control-request-method");
252
+ return event.req.method === "OPTIONS" && !!origin && !!accessControlRequestMethod;
253
+ }
254
+ function appendCorsPreflightHeaders(event, options) {
255
+ const headerGroups = [
256
+ createOriginHeaders(event, options),
257
+ createCredentialsHeaders(options),
258
+ createMethodsHeaders(event, options),
259
+ createAllowHeaderHeaders(event, options),
260
+ createMaxAgeHeader(options)
261
+ ];
262
+ const headers = Object.assign({}, ...headerGroups);
263
+ const varyValues = headerGroups.map((group) => group.vary).filter(Boolean);
264
+ if (varyValues.length > 0) headers.vary = varyValues.join(", ");
265
+ setCorsHeaders(event, headers);
266
+ }
267
+ function appendCorsHeaders(event, options) {
268
+ setCorsHeaders(event, {
269
+ ...createOriginHeaders(event, options),
270
+ ...createCredentialsHeaders(options),
271
+ ...createExposeHeaders(options)
272
+ });
273
+ }
274
+ function setCorsHeaders(event, headers) {
275
+ for (const [key, value] of Object.entries(headers)) if (key === "vary") {
276
+ event.res.headers.append(key, value);
277
+ event.res.errHeaders.append(key, value);
278
+ } else {
279
+ event.res.headers.set(key, value);
280
+ event.res.errHeaders.set(key, value);
281
+ }
282
+ }
283
+ function handleCors(event, options) {
284
+ const _options = resolveCorsOptions(options);
285
+ if (isPreflightRequest(event)) {
286
+ appendCorsPreflightHeaders(event, _options);
287
+ return noContent(_options.preflight.statusCode);
288
+ }
289
+ appendCorsHeaders(event, _options);
290
+ return false;
291
+ }
292
+ export { appendCorsHeaders, appendCorsPreflightHeaders, base64Decode, base64Encode, handleCors, html, isCorsOriginAllowed, isPreflightRequest, iterable, noContent, onDispose, raw, redirect, redirectBack, textDecoder, textEncoder, writeEarlyHints };
@@ -0,0 +1,117 @@
1
+ # Getting Started
2
+
3
+ > Get started with H3.
4
+
5
+ > [!IMPORTANT]
6
+ > You are currently reading H3 v2 docs. See [v1.h3.dev](https://v1.h3.dev/) for legacy docs.
7
+
8
+ ## Overview
9
+
10
+ ⚡ H3 (short for H(TTP), pronounced as /eɪtʃθriː/, like h-3) is a lightweight, fast, and composable server framework for modern JavaScript runtimes. It is based on web standard primitives such as [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL), and [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers). You can integrate H3 with any compatible runtime or [mount](/guide/api/h3#h3mount) other web-compatible handlers to H3 with almost no added latency.
11
+
12
+ H3 is designed to be extendable and composable. Instead of providing one big core, you start with a lightweight [H3 instance](/guide/api/h3) and then import built-in, tree-shakable [utilities](/utils) or bring your own for more functionality.
13
+ Composable utilities has several advantages:
14
+
15
+ - The server only includes used code and runs them exactly where is needed.
16
+ - Application size can scale better. Usage of utilities is explicit and clean, with less global impact.
17
+ - H3 is minimally opinionated and won't limit your choices.
18
+ All utilities, share an [H3Event](/guide/api/h3event) context.
19
+
20
+ <read-more></read-more>
21
+
22
+ ## Quick Start
23
+
24
+ > [!TIP]
25
+ > You try H3 online [on ⚡️ Stackblitz ](https://stackblitz.com/github/h3js/h3/tree/main/playground?file=server.mjs).
26
+
27
+ Install `h3` as a dependency:
28
+
29
+ <pm-install></pm-install>
30
+
31
+ Create a new file for server entry:
32
+
33
+ ```ts [server.mjs]
34
+ import { H3, serve } from "h3";
35
+
36
+ const app = new H3().get("/", (event) => "⚡️ Tadaa!");
37
+
38
+ serve(app, { port: 3000 });
39
+ ```
40
+
41
+ Then, run the server using your favorite runtime:
42
+
43
+ <code-group>
44
+
45
+ ```bash [node]
46
+ node --watch ./server.mjs
47
+ ```
48
+
49
+ ```bash [deno]
50
+ deno run -A --watch ./server.mjs
51
+ ```
52
+
53
+ ```bash [bun]
54
+ bun run --watch server.mjs
55
+ ```
56
+ </code-group>
57
+
58
+ And tadaa! We have a web server running locally.
59
+
60
+ ### What Happened?
61
+
62
+ Okay, let's now break down our hello world example.
63
+
64
+ We first created an [H3](/guide/api/h3) app instance using `new H3()`:
65
+
66
+ ```ts
67
+ const app = new H3();
68
+ ```
69
+
70
+ [H3](/guide/api/h3) is a tiny class capable of [matching routes](/guide/basics/routing), [generating responses](/guide/basics/response) and calling [middleware](/guide/basics/middleware) and [global hooks](/guide/api/h3#global-hooks).
71
+
72
+ Then we add a route for handling HTTP GET requests to `/` path.
73
+
74
+ ```ts
75
+ app.get("/", (event) => {
76
+ return { message: "⚡️ Tadaa!" };
77
+ });
78
+ ```
79
+
80
+ <read-more></read-more>
81
+
82
+ We simply returned an object. H3 automatically [converts](/guide/basics/response#response-types) values into web responses.
83
+
84
+ <read-more></read-more>
85
+
86
+ Finally, we use `serve` method to start the server listener. Using `serve` method you can easily start an H3 server in various runtimes.
87
+
88
+ ```js
89
+ serve(app, { port: 3000 });
90
+ ```
91
+
92
+ > [!TIP]
93
+ > The `serve` method is powered by [💥 srvx](https://srvx.h3.dev/), a runtime-agnostic universal server listener based on web standards that works seamlessly with [Deno](https://deno.com/), [Node.js](https://nodejs.org/) and [Bun](https://bun.sh/).
94
+
95
+ We also have [`app.fetch`](/guide/api/h3#h3fetch) which can be directly used to run H3 apps in any web-compatible runtime or even directly called for testing purposes.
96
+
97
+ <read-more></read-more>
98
+
99
+ ```js
100
+ import { H3, serve } from "h3";
101
+
102
+ const app = new H3().get("/", () => "⚡️ Tadaa!");
103
+
104
+ // Test without listening
105
+ const response = await app.request("/");
106
+ console.log(await response.text());
107
+ ```
108
+
109
+ You can directly import `h3` library from CDN alternatively. This method can be used for Bun, Deno and other runtimes such as Cloudflare Workers.
110
+
111
+ ```js
112
+ import { H3 } from "https://esm.sh/h3";
113
+
114
+ const app = new H3().get("/", () => "⚡️ Tadaa!");
115
+
116
+ export const fetch = app.fetch;
117
+ ```
@@ -0,0 +1,68 @@
1
+ # Request Lifecycle
2
+
3
+ > H3 dispatches incoming web requests to final web responses.
4
+
5
+ Below is an overview of what happens in a H3 server from when an HTTP request arrives until a response is generated.
6
+
7
+ ## 1. Incoming Request
8
+
9
+ When An HTTP request is made by Browser or [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), server fetch handler receives a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object.
10
+
11
+ ```mermaid
12
+ %%{init: {'theme':'neutral'}}%%
13
+ flowchart LR
14
+ A1["<code>fetch(request)</code>"] --> A2["<code>server.fetch(request)</code>"]
15
+
16
+ click A2 "/guide/api/h3#h3fetch"
17
+ ```
18
+
19
+ > [!TIP]
20
+ > ​[💥 Srvx](https://srvx.h3.dev) provides unified `server.fetch` interface and adds [Node.js compatibility](https://srvx.h3.dev/guide/node).
21
+
22
+ ## 2. Accept Request
23
+
24
+ H3 Initializes an [`H3Event`](/guide/api/h3event) instance from incoming request, calls [`onRequest`](/guide/api/h3#global-hooks) global hook and finally [`H3.handler`](/guide/api/h3#h3handler) with the initialized event.
25
+
26
+ ```mermaid
27
+ %%{init: {'theme':'neutral'}}%%
28
+ flowchart LR
29
+ B1["<code>new H3Event(request)</code>"] --> B2["<code>onRequest(event)</code>"] --> B3["<code>h3.handler(event)</code>"]
30
+
31
+ click B1 "/guide/api/h3event"
32
+ click B2 "/guide/api/h3#global-hooks"
33
+ click B3 "/guide/api/h3#apphandler"
34
+ ```
35
+
36
+ ## 3. Dispatch Request
37
+
38
+ H3 [matches route](/guide/basics/routing) based on `request.url` and `request.method`, calls global [middleware](/guide/basics/middleware) and finally matched route handler function with event.
39
+
40
+ ```mermaid
41
+ %%{init: {'theme':'neutral'}}%%
42
+ sequenceDiagram
43
+ participant MiddlewareA as Middleware1(event, next)
44
+ participant MiddlewareB as Middleware2(event, next)
45
+ participant Route as RouteHandler(event)
46
+
47
+ MiddlewareA->>+MiddlewareB: await next()
48
+ MiddlewareB->>+Route: await next()
49
+ Route-->>-MiddlewareB: rawBody
50
+ MiddlewareB-->>-MiddlewareA: rawBody
51
+
52
+ ```
53
+
54
+ > [!TIP]
55
+ > 🚀 Internally, H3 uses srvx `FastURL` instead of `new URL(req.url).pathname`.
56
+
57
+ ## 4. Send Response
58
+
59
+ H3 [converts](/guide/basics/response#response-types) returned value and [prepared headers](/guide/basics/response#preparing-response) into a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), calls [`onResponse`](/guide/api/h3#global-hooks) global hook and finally returns response back to the server fetch handler.
60
+
61
+ ```mermaid
62
+ %%{init: {'theme':'neutral'}}%%
63
+ flowchart LR
64
+ D1["Returned Value => Response"] --> D2["<code>onResponse(response)</code>"] --> D3["Response"]
65
+
66
+ click D1 "/guide/basics/response"
67
+ click D2 "/guide/api/h3#global-hooks"
68
+ ```
@@ -0,0 +1,167 @@
1
+ # Routing
2
+
3
+ > Each request is matched to one (most specific) route handler.
4
+
5
+ ## Adding Routes
6
+
7
+ You can register route [handlers](/guide/basics/handler) to [H3 instance](/guide/api/h3) using [`H3.on`](/guide/api/h3#h3on), [`H3.[method]`](/guide/api/h3#h3method), or [`H3.all`](/guide/api/h3#h3all).
8
+
9
+ > [!TIP]
10
+ > Router is powered by [🌳 Rou3](https://github.com/h3js/rou3), an ultra-fast and tiny route matcher engine.
11
+
12
+ **Example:** Register a route to match requests to the `/hello` endpoint with HTTP **GET** method.
13
+
14
+ - Using [`H3.[method]`](/guide/api/h3#h3method)
15
+
16
+ ```js
17
+ app.get("/hello", () => "Hello world!");
18
+ ```
19
+
20
+ - Using [`H3.on`](/guide/api/h3#h3on)
21
+
22
+ ```js
23
+ app.on("GET", "/hello", () => "Hello world!");
24
+ ```
25
+
26
+
27
+ You can register multiple event handlers for the same route with different methods:
28
+
29
+ ```js
30
+ app
31
+ .get("/hello", () => "GET Hello world!")
32
+ .post("/hello", () => "POST Hello world!")
33
+ .all("/hello", () => "Any other method!");
34
+ ```
35
+
36
+ You can also use [`H3.all`](/guide/api/h3#h3all) method to register a route accepting any HTTP method:
37
+
38
+ ```js
39
+ app.all("/hello", (event) => `This is a ${event.req.method} request!`);
40
+ ```
41
+
42
+ ## HEAD Requests
43
+
44
+ Following [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-head), `HEAD` requests automatically match the corresponding `GET` route and run its handler, but the response body is omitted (only the headers and status are sent). You don't need to register a separate `HEAD` handler:
45
+
46
+ ```js
47
+ app.get("/hello", () => "Hello world!");
48
+
49
+ // HEAD /hello → 200 with the same headers as GET, but an empty body
50
+ ```
51
+
52
+ Register an explicit `HEAD` handler when you want to override this — for example, to skip computing the body:
53
+
54
+ ```js
55
+ app.head("/hello", (event) => {
56
+ event.res.headers.set("content-length", "12");
57
+ return null;
58
+ });
59
+ ```
60
+
61
+ An explicit `head()` route always takes precedence over the automatic `GET` fallback.
62
+
63
+ ## HTTP `QUERY` Method
64
+
65
+ H3 supports the [HTTP `QUERY` method (RFC 10008)](https://www.rfc-editor.org/rfc/rfc10008) as a first-class method. `QUERY` is like `GET` — **safe, idempotent, and cacheable** — but carries a request body (with a `Content-Type`), closing the long-standing "GET with a body" gap. It's ideal for complex read operations where filters don't fit in a URL.
66
+
67
+ Register a `QUERY` handler with `app.query()` (or `app.on("QUERY", …)`) and read the request body as usual:
68
+
69
+ ```js
70
+ import { readBody } from "h3";
71
+
72
+ app.query("/search", async (event) => {
73
+ const criteria = await readBody(event); // read the query body
74
+ return runSearch(criteria);
75
+ });
76
+ ```
77
+
78
+ Because `QUERY` carries an attacker-controllable body, [body-size limits](/utils/request#assertbodysizeevent-limit) apply just like `POST`.
79
+
80
+ Two utilities help implement the RFC:
81
+
82
+ - [`requireContentType(event, acceptedTypes)`](/utils/request#requirecontenttypeevent-acceptedtypes) — assert the request `Content-Type` (`400`/`415`/`422`).
83
+ - [`appendAcceptQuery(event, mediaTypes)`](/utils/request#appendacceptqueryevent-mediatypes) — advertise accepted query formats via the `Accept-Query` response header.
84
+
85
+ > [!NOTE]
86
+ `QUERY` is treated like `GET` for [conditional caching](/utils/request#handlecacheheadersevent-opts) (`304` responses via `handleCacheHeaders`), and [`proxy`](/utils/proxy) forwards it **with** its body. Unlike `GET`, `QUERY` is **not** CORS-safelisted, so browsers send a preflight — if you pass an explicit `methods` allowlist to [`handleCors`](/utils/security#handlecorsevent-options), include `"QUERY"`.
87
+
88
+ <read-more>
89
+
90
+ See the [HTTP `QUERY` method example](/examples/handle-query) for a runnable `/books` resource that validates the `Content-Type` and advertises a cacheable `GET` alternative.
91
+ </read-more>
92
+
93
+ ## Route Patterns
94
+
95
+ A route pattern is a **pathname**, not a URL: the same shape as `event.url.pathname`, plus [rou3](https://github.com/h3js/rou3) syntax. [`H3.on`](/guide/api/h3#h3on), [`H3.[method]`](/guide/api/h3#h3method), [`H3.all`](/guide/api/h3#h3all), [`H3.use(route, ...)`](/guide/api/h3#h3use), [`H3.mount`](/guide/api/h3#h3mount) and [`removeRoute`](/utils/more#removerouteapp-method-route) normalize it identically, so a middleware registered with the same string as a route always guards that route.
96
+
97
+ Normalization rules:
98
+
99
+ - A leading `/` is added when missing (`"hello"` → `/hello`).
100
+ - A URL is **rejected** (`app.get("http://example.com/admin")` throws). An authority is never silently dropped: `//admin` registers as the two-segment path `//admin`, not as `/`.
101
+ - `.` and `..` segments resolve exactly as the URL parser resolves them in a request path (`/admin/../admin` → `/admin`).
102
+ - Characters that a request pathname always carries percent-encoded are encoded: space, non-ASCII, control characters, `"`, `#`, `<`, `>` and ```. So `app.get("/café")` registers `/caf%C3%A9` — what the browser actually sends.
103
+ - Needless escapes are decoded to the literal that the request pathname is canonicalized to (`/%40handle` → `/@handle`, see [Security utils](/utils/security)).
104
+ Characters that carry rou3 meaning are left exactly as written, including the escape `\` (never valid in a request pathname). To match `?`, `{`, `}` or `^` **literally**, write it percent-encoded:
105
+
106
+ ```js
107
+ app.get("/u/:id?", () => "optional param"); // rou3 syntax, kept as written
108
+ app.get("/x%3Fy", () => "literal ?"); // matches the path a client sends for /x?y
109
+ ```
110
+
111
+ > [!NOTE]
112
+ > Non-ASCII text mixes freely with dynamic syntax — `app.get("/café/:id")` registers `/caf%C3%A9/:id` and matches `/café/42` — with two exceptions, both from the encoded form reaching rou3's own syntax. A **param name** must be ASCII (`[\w-]`): `/:naïve` becomes `/:na%C3%AFve`, which rou3 reads as a param named `na` followed by the literal `%C3%AFve`. And inside a `(...)` group, only literal text and alternation survive encoding (`(café|thé)` works); a **character class** does not, since `[é]` becomes `[%C3%A9]` — write the encoded alternation `(?:%C3%A9)` instead.
113
+
114
+ ## Dynamic Routes
115
+
116
+ You can define dynamic route parameters using `:` prefix:
117
+
118
+ ```js
119
+ // [GET] /hello/Bob => "Hello, Bob!"
120
+ app.get("/hello/:name", (event) => {
121
+ return `Hello, ${event.context.params.name}!`;
122
+ });
123
+ ```
124
+
125
+ To make a parameter optional, add `?`:
126
+
127
+ ```js
128
+ // Matches /hello and /hello/Bob
129
+ app.get("/hello/:name?", (event) => `Hello, ${event.context.params?.name ?? "you"}!`);
130
+ ```
131
+
132
+ ## Wildcard Routes
133
+
134
+ Adding `/hello/:name` route will match `/hello/world` or `/hello/123`. But it will not match `/hello/foo/bar`.
135
+ When you need to match multiple levels of sub routes, you can use a `**` (or `*`) catch-all:
136
+
137
+ ```js
138
+ app.get("/hello/**", (event) => `Hello ${event.context.params?.[0] ?? ""}!`);
139
+ ```
140
+
141
+ This will match `/hello`, `/hello/world`, `/hello/123`, `/hello/world/123`, etc.
142
+
143
+ > [!NOTE]
144
+ > An unnamed `**` or `*` stores the matched sub-path as a single string keyed by position (`params[0]`), and is left out when it matches nothing. `params._` still works for `**` but is deprecated. A named catch-all (`/hello/**:path`) stores it as `params.path`, but needs at least one segment, so it does not match `/hello`.
145
+
146
+ > A route can have only one catch-all (`*`, `**`, `:name+`, `:name*` or `(.*)`).
147
+
148
+ ## Route Meta
149
+
150
+ You can define optional route meta when registering them, accessible from any middleware.
151
+
152
+ ```js
153
+ import { H3 } from "h3";
154
+
155
+ const app = new H3();
156
+
157
+ app.use((event) => {
158
+ console.log(event.context.matchedRoute?.meta); // { auth: true }
159
+ });
160
+
161
+ app.get("/", (event) => "Hi!", { meta: { auth: true } });
162
+ ```
163
+
164
+ <read-more>
165
+
166
+ It is also possible to add route meta when defining them using `defineHandler` object syntax.
167
+ </read-more>