@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
@@ -0,0 +1,175 @@
1
+ # Security
2
+
3
+ > H3 security utilities.
4
+
5
+ ## Authentication
6
+
7
+ ### `basicAuth(opts)`
8
+
9
+ Create a basic authentication middleware.
10
+
11
+ **Example:**
12
+
13
+ ```ts
14
+ import { H3, serve, basicAuth } from "h3";
15
+ const auth = basicAuth({ password: "test" });
16
+ app.get("/", (event) => `Hello ${event.context.basicAuth?.username}!`, [auth]);
17
+ serve(app, { port: 3000 });
18
+ ```
19
+
20
+ ### `requireBasicAuth(event, opts)`
21
+
22
+ Apply basic authentication for current request.
23
+
24
+ **Example:**
25
+
26
+ ```ts
27
+ import { defineHandler, requireBasicAuth } from "h3";
28
+ export default defineHandler(async (event) => {
29
+ await requireBasicAuth(event, { password: "test" });
30
+ return `Hello, ${event.context.basicAuth.username}!`;
31
+ });
32
+ ```
33
+
34
+ ## Session
35
+
36
+ ### `clearSession(event, config)`
37
+
38
+ Clear the session data for the current request.
39
+
40
+ ### `getSession(event, config)`
41
+
42
+ Get the session for the current request.
43
+
44
+ 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.
45
+
46
+ ### `sealSession(event, config)`
47
+
48
+ Encrypt and sign the session data for the current request.
49
+
50
+ ### `unsealSession(_event, config, sealed)`
51
+
52
+ Decrypt and verify the session data for the current request.
53
+
54
+ ### `updateSession(event, config, update?)`
55
+
56
+ Update the session data for the current request.
57
+
58
+ ### `useSession(event, config)`
59
+
60
+ Create a session manager for the current request.
61
+
62
+ 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.
63
+
64
+ ## Fingerprint
65
+
66
+ ### `getRequestFingerprint(event, opts)`
67
+
68
+ Get a unique fingerprint for the incoming request.
69
+
70
+ ## CORS
71
+
72
+ ### `appendCorsHeaders(event, options)`
73
+
74
+ Append CORS headers to the response.
75
+
76
+ ### `appendCorsPreflightHeaders(event, options)`
77
+
78
+ Append CORS preflight headers to the response.
79
+
80
+ ### `handleCors(event, options)`
81
+
82
+ Handle CORS for the incoming request.
83
+
84
+ If the incoming request is a CORS preflight request, it will append the CORS preflight headers and send a 204 response.
85
+
86
+ If return value is not `false`, the request is handled and no further action is needed.
87
+
88
+ **Example:**
89
+
90
+ ```ts
91
+ const app = new H3();
92
+ app.all("/", async (event) => {
93
+ const corsRes = handleCors(event, {
94
+ origin: "*",
95
+ preflight: {
96
+ statusCode: 204,
97
+ },
98
+ methods: "*",
99
+ });
100
+ if (corsRes !== false) {
101
+ return corsRes;
102
+ }
103
+ // Your code here
104
+ });
105
+ ```
106
+
107
+ ### `isCorsOriginAllowed(origin, options)`
108
+
109
+ Check if the origin is allowed.
110
+
111
+ ### `isPreflightRequest(event)`
112
+
113
+ Check if the incoming request is a CORS preflight request.
114
+
115
+ ## Path
116
+
117
+ ### `isCanonicalPath(path, opts?)`
118
+
119
+ 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`.
120
+
121
+ 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.
122
+
123
+ 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.
124
+
125
+ 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`).
126
+
127
+ ### `normalizeRoute(route)`
128
+
129
+ 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.
130
+
131
+ `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.
132
+
133
+ 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.
134
+
135
+ Idempotent. Throws on an absolute URL (`http://…`): a route pattern is a pathname, never a URL.
136
+
137
+ **Example:**
138
+
139
+ ```ts
140
+ normalizeRoute("/について/**"); // "/%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/**"
141
+ ```
142
+
143
+ ### `resolveDotSegments(path, opts?)`
144
+
145
+ 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`).
146
+
147
+ 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 `../`.
148
+
149
+ `%2f`/`%5c` (encoded path separators) are left untouched by default — see {@link ResolveDotSegmentsOptions.decodeSlashes}.
150
+
151
+ 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 <u>leading</u> 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}.
152
+
153
+ ## Route params
154
+
155
+ 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:
156
+
157
+ - 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.
158
+ - 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.
159
+
160
+ ```ts
161
+ app.get("/files/**:rest", (event) => {
162
+ // GET /files/%252e%252e/x
163
+ getRouterParams(event); // { rest: "%252e%252e/x" }
164
+ getRouterParams(event, { decode: true }); // { rest: "%2e%2e/x" }
165
+
166
+ // GET /files/%2500
167
+ getRouterParams(event, { decode: true }); // { rest: "%00" }
168
+
169
+ // GET /files/a%252fb — separators stay encoded at every depth
170
+ getRouterParams(event, { decode: true }); // { rest: "a%252fb" }
171
+ });
172
+ ```
173
+
174
+ > [!IMPORTANT]
175
+ 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`](#resolvedotsegmentspath-opts) rather than by decoding further.
@@ -0,0 +1,57 @@
1
+ # Proxy
2
+
3
+ > H3 proxy utilities.
4
+
5
+ ### `fetchWithEvent(event, url, init?)`
6
+
7
+ Make a fetch request carrying the event's context.
8
+
9
+ Behavior depends on the target:
10
+
11
+ 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`, ...). It always resolves against the app's own origin: a leading separator run (`//host/x`, `/\host/x`, and C0-interleaved forms like `/\thost/x` that the URL parser strips down to one) is collapsed to a single `/` rather than read as an authority.
12
+
13
+ An **external** `url` is sent with native `fetch(url, init)` **unchanged** — the event's headers and context are <u>not</u> 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.
14
+
15
+ **Security:** Never pass unsanitized user input as the `url`. Callers are responsible for validating and restricting the URL.
16
+
17
+ ### `getProxyRequestHeaders(event)`
18
+
19
+ Get the request headers object without headers known to cause issues when proxying.
20
+
21
+ ### `proxy(event, target, opts)`
22
+
23
+ Make a proxy request to a target URL and send the response back to the client.
24
+
25
+ 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).
26
+
27
+ 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.)
28
+
29
+ **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.
30
+
31
+ **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).
32
+
33
+ **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).
34
+
35
+ ### `proxyRequest(event, target, opts)`
36
+
37
+ Proxy the incoming request to a target URL.
38
+
39
+ 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. Such a target always resolves against the app's own origin: a leading separator run (`//host/x`, `/\host/x`, and C0-interleaved forms like `/\thost/x` that the URL parser strips down to one) is collapsed to a single `/` rather than read as an authority.
40
+
41
+ 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.
42
+
43
+ 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.
44
+
45
+ **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.
46
+
47
+ **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.)
48
+
49
+ **Example:**
50
+
51
+ ```ts
52
+ app.all("/proxy", async (event) => {
53
+ const body = await event.req.clone().json(); // read from the clone
54
+ // ...inspect body...
55
+ return proxyRequest(event, "/target"); // original stream still intact
56
+ });
57
+ ```
@@ -0,0 +1,75 @@
1
+ # MCP
2
+
3
+ > H3 MCP related utils.
4
+
5
+ ### `defineJsonRpcHandler()`
6
+
7
+ Creates an H3 event handler that implements the JSON-RPC 2.0 specification.
8
+
9
+ **Security defaults:** requests must have a JSON `Content-Type` (CSRF, see `validateContentType`), cross-origin requests are rejected (CSRF and DNS rebinding, see `allowedOrigins`), and batches are capped at 50 requests (fan-out amplification, see `maxBatchSize`).
10
+
11
+ **Example:**
12
+
13
+ ```ts
14
+ app.post(
15
+ "/rpc",
16
+ defineJsonRpcHandler({
17
+ methods: {
18
+ echo: ({ params }, event) => {
19
+ return `Received \`${params}\` on path \`${event.url.pathname}\``;
20
+ },
21
+ sum: ({ params }, event) => {
22
+ return params.a + params.b;
23
+ },
24
+ },
25
+ }),
26
+ );
27
+ ```
28
+
29
+ ### `defineJsonRpcWebSocketHandler()`
30
+
31
+ Creates an H3 event handler that implements JSON-RPC 2.0 over WebSocket.
32
+
33
+ This is an opt-in feature that allows JSON-RPC communication over WebSocket connections for bi-directional messaging. Each incoming WebSocket text message is processed as a JSON-RPC request, and responses are sent back to the peer.
34
+
35
+ **Security:** unlike `defineJsonRpcHandler()`, this does not check the request `Origin`. WebSocket upgrades are not subject to CORS, so a page on any origin can open a connection carrying the visitor's cookies (cross-site WebSocket hijacking). Validate `Origin` in the `upgrade` hook and throw a `Response` to abort the connection.
36
+
37
+ **Example:**
38
+
39
+ ```ts
40
+ app.get(
41
+ "/rpc/ws",
42
+ defineJsonRpcWebSocketHandler({
43
+ methods: {
44
+ echo: ({ params }) => {
45
+ return `Received: ${Array.isArray(params) ? params[0] : params?.message}`;
46
+ },
47
+ sum: ({ params }) => {
48
+ return params.a + params.b;
49
+ },
50
+ },
51
+ }),
52
+ );
53
+ ```
54
+
55
+ **Example:**
56
+
57
+ ```ts
58
+ // With additional WebSocket hooks
59
+ app.get(
60
+ "/rpc/ws",
61
+ defineJsonRpcWebSocketHandler({
62
+ methods: {
63
+ greet: ({ params }) => `Hello, ${params.name}!`,
64
+ },
65
+ hooks: {
66
+ open(peer) {
67
+ console.log(`Peer connected: ${peer.id}`);
68
+ },
69
+ close(peer, details) {
70
+ console.log(`Peer disconnected: ${peer.id}`, details);
71
+ },
72
+ },
73
+ }),
74
+ );
75
+ ```
@@ -0,0 +1,117 @@
1
+ # More utils
2
+
3
+ > More H3 utilities.
4
+
5
+ ## Base
6
+
7
+ ### `withBase(base, input)`
8
+
9
+ Returns a new event handler that removes the base url of the event before calling the original handler.
10
+
11
+ **Example:**
12
+
13
+ ```ts
14
+ const api = new H3()
15
+ .get("/", () => "Hello API!");
16
+ const app = new H3();
17
+ .use("/api/**", withBase("/api", api.handler));
18
+ ```
19
+
20
+ ## Event
21
+
22
+ ### `getEventContext(event)`
23
+
24
+ Gets the context of the event, if it does not exists, initializes a new context on `req.context`.
25
+
26
+ ### `isEvent(input)`
27
+
28
+ Checks if the input is an H3Event object.
29
+
30
+ ### `isHTTPEvent(input)`
31
+
32
+ Checks if the input is an object with `{ req: Request }` signature.
33
+
34
+ ### `mockEvent(_request, options?)`
35
+
36
+ ## Middleware
37
+
38
+ ### `bodyLimit(limit)`
39
+
40
+ Define a middleware that limits the request body size to the specified limit.
41
+
42
+ 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.
43
+
44
+ ### `onError(hook)`
45
+
46
+ Define a middleware that runs when an error occurs.
47
+
48
+ You can return a new Response from the handler to gracefully handle the error.
49
+
50
+ ### `onRequest(hook)`
51
+
52
+ Define a middleware that runs on each request.
53
+
54
+ ### `onResponse(hook)`
55
+
56
+ Define a middleware that runs after Response is generated.
57
+
58
+ You can return a new Response from the handler to replace the original response.
59
+
60
+ ## WebSocket
61
+
62
+ ### `defineWebSocket(hooks)`
63
+
64
+ Define WebSocket hooks.
65
+
66
+ **Example:**
67
+
68
+ ```ts
69
+ const hooks = defineWebSocket({
70
+ open: (peer) => peer.send("Welcome!"),
71
+ message: (peer, message) => peer.send(message.text()),
72
+ close: (peer) => console.log("closed", peer),
73
+ });
74
+ ```
75
+
76
+ ### `defineWebSocketHandler(http?)`
77
+
78
+ Define WebSocket event handler.
79
+
80
+ By default, non-upgrade (plain HTTP) requests receive a `426 Upgrade Required` response. Pass an `http` handler to serve those requests instead, allowing the same route to handle both WebSocket upgrades and regular HTTP requests. WebSocket upgrade requests always go to `hooks`.
81
+
82
+ Note: the `http` handler only handles non-upgrade requests. To reject or customize the upgrade handshake itself, use the crossws `upgrade` hook instead.
83
+
84
+ **Example:**
85
+
86
+ ```ts
87
+ // WebSocket-only route (non-upgrade requests get `426 Upgrade Required`)
88
+ app.get(
89
+ "/_ws",
90
+ defineWebSocketHandler({
91
+ message: (peer, message) => peer.send(message.text()),
92
+ }),
93
+ );
94
+ ```
95
+
96
+ **Example:**
97
+
98
+ ```ts
99
+ // Handle both WebSocket upgrades and plain HTTP on the same route
100
+ app.get(
101
+ "/_ws",
102
+ defineWebSocketHandler(
103
+ { message: (peer, message) => peer.send(message.text()) },
104
+ () => "Send a WebSocket upgrade request to connect.",
105
+ ),
106
+ );
107
+ ```
108
+
109
+ ## Adapters
110
+
111
+ ### `defineNodeHandler(handler)`
112
+
113
+ ### `defineNodeMiddleware(handler)`
114
+
115
+ ### `fromNodeHandler(handler)`
116
+
117
+ ### `fromWebHandler(handler)`
@@ -0,0 +1,48 @@
1
+ # Community
2
+
3
+ > H3 utils from community.
4
+
5
+ You can use external H3 event utilities made by the community.
6
+
7
+ This section is placeholder for any new H3 version 2 compatible community library.
8
+
9
+
10
+
11
+ > [!TIP]
12
+ > 💛 PR is more than welcome to list yours.
13
+
14
+ ## `apitally`
15
+
16
+ [Apitally](https://apitally.io/h3) is a simple API monitoring, analytics, and request logging tool with a plugin for H3. See setup guide [here](https://docs.apitally.io/frameworks/h3).
17
+
18
+ <read-more></read-more>
19
+
20
+ ## `H3ravel Framework`
21
+
22
+ [H3ravel Framework](https://h3ravel.toneflix.net) is a modern TypeScript runtime-agnostic web framework built on top of H3, designed to bring the elegance and developer experience of Laravel PHP to the JavaScript ecosystem. See the getting started guide [here](https://h3ravel.toneflix.net/guide/get-started).
23
+
24
+ <read-more></read-more>
25
+
26
+ ## `Intlify`
27
+
28
+ [Intlify](https://intlify.dev/) is a project that aims to improve Developer Experience in software internationalization. That project provides server-side frameworks, middleware, and utilities. About those, see the [here](https://github.com/intlify/srvmid)
29
+
30
+ <read-more></read-more>
31
+
32
+ ## `Clear Router`
33
+
34
+ Laravel-style routing system for H3 and Express.js. Clean route definitions, middleware support, and controller bindings with full TypeScript support.
35
+
36
+ <read-more></read-more>
37
+
38
+ ## `unjwt`
39
+
40
+ `unjwt` is a collection of low-level JWT utilities (JWS, JWE, JWK) built on the Web Crypto API, with zero runtime dependencies. It includes a dedicated H3 v2 adapter for header and cookie-based session management with support for encrypted (JWE) and signed (JWS) tokens.
41
+
42
+ <read-more></read-more>
43
+
44
+ ## `Arkstack`
45
+
46
+ [Arkstack](https://arkstack.toneflix.net) is a runtime-agnostic TypeScript backend framework for building structured, production-ready server applications with first-class support for H3.
47
+
48
+ <read-more></read-more>
@@ -0,0 +1,17 @@
1
+ # Examples
2
+
3
+ > Common examples for h3.
4
+
5
+ <read-more>
6
+
7
+ Check [`examples/` dir](https://github.com/h3js/h3/tree/main/examples) for more examples.
8
+ </read-more>
9
+
10
+ **Examples:**
11
+
12
+ - [Cookies](/examples/handle-cookie)
13
+ - [HTTP QUERY Method](/examples/handle-query)
14
+ - [Session](/examples/handle-session)
15
+ - [Static Assets](/examples/serve-static-assets)
16
+ - [Streaming Response](/examples/stream-response)
17
+ - [Validation](/examples/validate-data)
@@ -0,0 +1,67 @@
1
+ # Cookies
2
+
3
+ > Use cookies to store data on the client.
4
+
5
+ Handling cookies with H3 is straightforward. There is three utilities to handle cookies:
6
+
7
+ - `setCookie` to attach a cookie to the response.
8
+ - `getCookie` to get a cookie from the request.
9
+ - `deleteCookie` to clear a cookie from the response.
10
+
11
+ ## Set a Cookie
12
+ To set a cookie, you need to use `setCookie` in an event handler:
13
+
14
+ ```ts
15
+ import { setCookie } from "h3";
16
+
17
+ app.use(async (event) => {
18
+ setCookie(event, "name", "value", { maxAge: 60 * 60 * 24 * 7 });
19
+ return "";
20
+ });
21
+ ```
22
+
23
+ In the options, you can configure the [cookie flags](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie):
24
+
25
+ - `maxAge` to set the expiration date of the cookie in seconds.
26
+ - `expires` to set the expiration date of the cookie in a `Date` object.
27
+ - `path` to set the path of the cookie.
28
+ - `domain` to set the domain of the cookie.
29
+ - `secure` to set the `Secure` flag of the cookie.
30
+ - `httpOnly` to set the `HttpOnly` flag of the cookie.
31
+ - `sameSite` to set the `SameSite` flag of the cookie.
32
+ <read-more></read-more>
33
+
34
+ ## Get a Cookie
35
+
36
+ To get a cookie, you need to use `getCookie` in an event handler.
37
+
38
+ ```ts
39
+ import { getCookie } from "h3";
40
+
41
+ app.use(async (event) => {
42
+ const name = getCookie(event, "name");
43
+
44
+ // do something...
45
+
46
+ return "";
47
+ });
48
+ ```
49
+
50
+ This will return the value of the cookie if it exists, or `undefined` otherwise.
51
+
52
+ ## Delete a Cookie
53
+
54
+ To delete a cookie, you need to use `deleteCookie` in an event handler:
55
+
56
+ ```ts
57
+ import { deleteCookie } from "h3";
58
+
59
+ app.use(async (event) => {
60
+ deleteCookie(event, "name");
61
+ return "";
62
+ });
63
+ ```
64
+
65
+ The utility `deleteCookie` is a wrapper around `setCookie` with the value set to `""` and the `maxAge` set to `0`.
66
+
67
+ This will erase the cookie from the client.
@@ -0,0 +1,76 @@
1
+ # HTTP `QUERY` Method
2
+
3
+ > Accept safe, cacheable requests that carry a query in the body.
4
+
5
+ The [HTTP `QUERY` method (RFC 10008)](https://www.rfc-editor.org/rfc/rfc10008) is like `GET` — **safe, idempotent, and cacheable** — but carries a query in the request **body** with a `Content-Type`. It's the standard answer to "I need a GET, but my query is too large or too structured for the URL".
6
+
7
+ H3 supports `QUERY` as a first-class method via [`app.query()`](/guide/basics/routing#http-query-method), plus two helper utilities.
8
+
9
+ ## Register a `QUERY` Handler
10
+
11
+ Read the request body just like you would for a `POST`:
12
+
13
+ ```ts
14
+ import { readBody } from "h3";
15
+
16
+ app.query("/books", async (event) => {
17
+ const query = await readBody(event, { type: "text" });
18
+ return runSearch(query);
19
+ });
20
+ ```
21
+
22
+ Because `QUERY` carries an attacker-controllable body, [body-size limits](/utils/request#assertbodysizeevent-limit) apply just like `POST`.
23
+
24
+ ## Advertise Accepted Formats
25
+
26
+ Use [`appendAcceptQuery`](/utils/request#appendacceptqueryevent-mediatypes) to tell clients which query formats a resource understands. It sets the `Accept-Query` response header (a [Structured Fields](https://www.rfc-editor.org/rfc/rfc8941) List), and can be set on a plain `GET` too so clients can discover formats before sending a `QUERY`:
27
+
28
+ ```ts
29
+ import { appendAcceptQuery } from "h3";
30
+
31
+ app.get("/books", (event) => {
32
+ appendAcceptQuery(event, ["application/sql", "application/jsonpath"]);
33
+ // Accept-Query: application/sql, application/jsonpath
34
+ return "Send a QUERY request with a SQL or JSONPath body.";
35
+ });
36
+ ```
37
+
38
+ ## Validate the `Content-Type`
39
+
40
+ Use [`requireContentType`](/utils/request#requirecontenttypeevent-acceptedtypes) to enforce the RFC's error semantics. It returns the matched media type, or throws `400` (missing), `415` (unsupported), or `422` (malformed):
41
+
42
+ ```ts
43
+ import { requireContentType, readBody } from "h3";
44
+
45
+ app.query("/books", async (event) => {
46
+ const type = requireContentType(event, ["application/sql", "application/jsonpath"]);
47
+ const query = await readBody(event, { type: "text" });
48
+ return runQuery(type, query);
49
+ });
50
+ ```
51
+
52
+ ## Offer a Cacheable `GET` Alternative
53
+
54
+ A `QUERY` response is not addressable by URL, so browsers and CDNs can't cache it. RFC 10008 suggests pointing clients at an equivalent, cacheable `GET` via the `Content-Location` header. Stash the result under a stable id and let a client repeat the query with an ordinary, HTTP-cacheable `GET`:
55
+
56
+ ```ts
57
+ app.query("/books", async (event) => {
58
+ const result = runQuery(type, query);
59
+ const id = queryId(type, query); // stable hash of the query
60
+ cache.set(id, result);
61
+ event.res.headers.set("content-location", `/books/${id}`);
62
+ return result;
63
+ });
64
+ ```
65
+
66
+ ## Full Example
67
+
68
+ A self-contained, runnable demo — a `/books` resource that accepts SQL-ish and JSONPath queries, validates the `Content-Type`, and advertises a cacheable `GET` alternative. It also serves a small interactive page at `/`.
69
+
70
+ <read-more>
71
+
72
+ See the full [`examples/query.mjs`](https://github.com/h3js/h3/tree/main/examples/query.mjs) source, or run it locally with `node examples/query.mjs`.
73
+ </read-more>
74
+
75
+ > [!NOTE]
76
+ > 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"`.