@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.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/bin/h3.mjs +36 -0
- package/changes.json +5 -0
- package/dist/THIRD-PARTY-LICENSES.md +70 -0
- package/dist/_entries/bun.d.mts +6 -0
- package/dist/_entries/bun.mjs +16 -0
- package/dist/_entries/cloudflare.d.mts +6 -0
- package/dist/_entries/cloudflare.mjs +16 -0
- package/dist/_entries/deno.d.mts +6 -0
- package/dist/_entries/deno.mjs +16 -0
- package/dist/_entries/generic.d.mts +6 -0
- package/dist/_entries/generic.mjs +16 -0
- package/dist/_entries/node.d.mts +10 -0
- package/dist/_entries/node.mjs +19 -0
- package/dist/_entries/service-worker.d.mts +6 -0
- package/dist/_entries/service-worker.mjs +16 -0
- package/dist/_utils.mjs +240 -0
- package/dist/cache.mjs +599 -0
- package/dist/cache2.mjs +50 -0
- package/dist/cors.mjs +292 -0
- package/dist/docs/0.guide/0.index/index.md +117 -0
- package/dist/docs/0.guide/1.basics/0.lifecycle.md +68 -0
- package/dist/docs/0.guide/1.basics/1.routing.md +167 -0
- package/dist/docs/0.guide/1.basics/2.middleware.md +97 -0
- package/dist/docs/0.guide/1.basics/3.handler.md +165 -0
- package/dist/docs/0.guide/1.basics/4.response.md +171 -0
- package/dist/docs/0.guide/1.basics/5.error.md +117 -0
- package/dist/docs/0.guide/1.basics/6.nested-apps.md +57 -0
- package/dist/docs/0.guide/2.rules.md +698 -0
- package/dist/docs/0.guide/3.api/0.h3.md +144 -0
- package/dist/docs/0.guide/3.api/1.h3event.md +160 -0
- package/dist/docs/0.guide/4.advanced/0.plugins.md +50 -0
- package/dist/docs/0.guide/4.advanced/1.websocket.md +176 -0
- package/dist/docs/0.guide/4.advanced/2.nightly.md +13 -0
- package/dist/docs/1.utils/0.index/index.md +46 -0
- package/dist/docs/1.utils/1.request.md +447 -0
- package/dist/docs/1.utils/2.response.md +172 -0
- package/dist/docs/1.utils/3.cookie.md +33 -0
- package/dist/docs/1.utils/4.security.md +175 -0
- package/dist/docs/1.utils/5.proxy.md +57 -0
- package/dist/docs/1.utils/6.mcp.md +75 -0
- package/dist/docs/1.utils/7.more.md +117 -0
- package/dist/docs/1.utils/8.community.md +48 -0
- package/dist/docs/2.examples/0.index/index.md +17 -0
- package/dist/docs/2.examples/1.handle-cookie.md +67 -0
- package/dist/docs/2.examples/2.handle-query.md +76 -0
- package/dist/docs/2.examples/3.handle-session.md +210 -0
- package/dist/docs/2.examples/4.serve-static-assets.md +66 -0
- package/dist/docs/2.examples/5.stream-response.md +76 -0
- package/dist/docs/2.examples/6.validate-data.md +193 -0
- package/dist/docs/3.migration/0.index/index.md +204 -0
- package/dist/docs/README.md +37 -0
- package/dist/h3.d.mts +1669 -0
- package/dist/h3.mjs +1809 -0
- package/dist/index.d.mts +1634 -0
- package/dist/match.d.mts +123 -0
- package/dist/middleware.mjs +123 -0
- package/dist/normalize.mjs +645 -0
- package/dist/path.mjs +42 -0
- package/dist/proxy.mjs +254 -0
- package/dist/response.mjs +465 -0
- package/dist/rules/cache.d.mts +29 -0
- package/dist/rules/cache.mjs +163 -0
- package/dist/rules/compiler.d.mts +94 -0
- package/dist/rules/compiler.mjs +173 -0
- package/dist/rules/index.d.mts +77 -0
- package/dist/rules/index.mjs +34 -0
- package/dist/rules/proxy.d.mts +3 -0
- package/dist/rules/proxy.mjs +14 -0
- package/dist/tracing.d.mts +33 -0
- package/dist/tracing.mjs +89 -0
- package/package.json +148 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# Request
|
|
2
|
+
|
|
3
|
+
> H3 request utilities.
|
|
4
|
+
|
|
5
|
+
## Body
|
|
6
|
+
|
|
7
|
+
### `assertBodySize(event, limit)`
|
|
8
|
+
|
|
9
|
+
Asserts that the request body size is within the specified limit.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
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).
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
**Example:**
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
app.post("/", async (event) => {
|
|
21
|
+
assertBodySize(event, 10 * 1024 * 1024); // 10MB
|
|
22
|
+
const data = await event.req.formData();
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### `readBody(event, options?)`
|
|
27
|
+
|
|
28
|
+
Reads request body and tries to parse using JSON.parse or URLSearchParams.
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
**Example:**
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
app.post("/", async (event) => {
|
|
36
|
+
const body = await readBody(event);
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Example:**
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
app.post("/upload", async (event) => {
|
|
44
|
+
const body = await readBody(event, { type: "formData" });
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `readValidatedBody(event, validate)`
|
|
49
|
+
|
|
50
|
+
Tries to read the request body via `readBody`, then uses the provided validation schema or function and either throws a validation error or returns the result.
|
|
51
|
+
|
|
52
|
+
You can use a simple function to validate the body or use a Standard-Schema compatible library like `zod` to define a schema.
|
|
53
|
+
|
|
54
|
+
**Example:**
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
function validateBody(body: any) {
|
|
58
|
+
return typeof body === "object" && body !== null;
|
|
59
|
+
}
|
|
60
|
+
app.post("/", async (event) => {
|
|
61
|
+
const body = await readValidatedBody(event, validateBody);
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Example:**
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { z } from "zod";
|
|
69
|
+
const objectSchema = z.object({
|
|
70
|
+
name: z.string().min(3).max(20),
|
|
71
|
+
age: z.number({ coerce: true }).positive().int(),
|
|
72
|
+
});
|
|
73
|
+
app.post("/", async (event) => {
|
|
74
|
+
const body = await readValidatedBody(event, objectSchema);
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Example:**
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import * as v from "valibot";
|
|
82
|
+
app.post("/", async (event) => {
|
|
83
|
+
const body = await readValidatedBody(
|
|
84
|
+
event,
|
|
85
|
+
v.object({
|
|
86
|
+
name: v.pipe(v.string(), v.minLength(3), v.maxLength(20)),
|
|
87
|
+
age: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
88
|
+
}),
|
|
89
|
+
{
|
|
90
|
+
onError: ({ issues }) => ({
|
|
91
|
+
statusText: "Custom validation error",
|
|
92
|
+
message: v.summarize(issues),
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Query (HTTP `QUERY` method)
|
|
100
|
+
|
|
101
|
+
Utilities for the [HTTP `QUERY` method (RFC 10008)](https://www.rfc-editor.org/rfc/rfc10008): advertise the query formats a resource accepts and validate the request `Content-Type`.
|
|
102
|
+
|
|
103
|
+
### `appendAcceptQuery(event, mediaTypes)`
|
|
104
|
+
|
|
105
|
+
Advertise the query formats a resource accepts by setting the `Accept-Query` response header (RFC 10008, HTTP `QUERY` method).
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
**Example:**
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
app.query("/search", (event) => {
|
|
113
|
+
appendAcceptQuery(event, ["application/sql;charset=UTF-8", "application/jsonpath"]);
|
|
114
|
+
// Accept-Query: application/sql;charset="UTF-8", application/jsonpath
|
|
115
|
+
return handleSearch(event);
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `requireContentType(event, acceptedTypes)`
|
|
120
|
+
|
|
121
|
+
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.
|
|
122
|
+
|
|
123
|
+
Throws:
|
|
124
|
+
|
|
125
|
+
- `400 Bad Request` if the `Content-Type` header is missing.
|
|
126
|
+
|
|
127
|
+
- `422 Unprocessable Content` if the `Content-Type` header is malformed.
|
|
128
|
+
|
|
129
|
+
- `415 Unsupported Media Type` if the media type is not accepted.
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
Accepted types may use wildcards: `*` / `*/*` match anything and `type/*` matches any subtype of `type`.
|
|
133
|
+
|
|
134
|
+
**Example:**
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
app.query("/search", async (event) => {
|
|
138
|
+
requireContentType(event, ["application/sql", "application/jsonpath"]);
|
|
139
|
+
const body = await readBody(event, { type: "text" });
|
|
140
|
+
// ...
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Cache
|
|
145
|
+
|
|
146
|
+
### `handleCacheHeaders(event, opts)`
|
|
147
|
+
|
|
148
|
+
Check request caching headers (`If-None-Match`, `If-Modified-Since`) and add caching headers (Last-Modified, ETag, Cache-Control).
|
|
149
|
+
|
|
150
|
+
Note: `public` is added by default, but never alongside a caller-supplied `private`/`no-store` directive, so passing `cacheControls: ["private"]` no longer produces a contradictory `public, private`.
|
|
151
|
+
|
|
152
|
+
## More Request Utils
|
|
153
|
+
|
|
154
|
+
### `assertMethod(event, expected, allowHead?)`
|
|
155
|
+
|
|
156
|
+
Asserts that the incoming request method is of the expected type using `isMethod`.
|
|
157
|
+
|
|
158
|
+
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.
|
|
159
|
+
|
|
160
|
+
If `allowHead` is `true`, it will allow `HEAD` requests to pass if the expected method is `GET`.
|
|
161
|
+
|
|
162
|
+
**Example:**
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
app.get("/", (event) => {
|
|
166
|
+
assertMethod(event, "GET");
|
|
167
|
+
// Handle GET request, otherwise throw 405 error
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `getQuery(event)`
|
|
172
|
+
|
|
173
|
+
Get parsed query string object from the request URL.
|
|
174
|
+
|
|
175
|
+
To access the raw (unparsed) query string, for example to parse nested queries with a custom parser such as `qs`, use `event.url.search` directly.
|
|
176
|
+
|
|
177
|
+
**Example:**
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
app.get("/", (event) => {
|
|
181
|
+
const query = getQuery(event); // { key: "value", key2: ["value1", "value2"] }
|
|
182
|
+
const rawQuery = event.url.search; // "?key=value&key2=value1&key2=value2"
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `getRequestHost(event, opts: { xForwardedHost? })`
|
|
187
|
+
|
|
188
|
+
Get the request hostname.
|
|
189
|
+
|
|
190
|
+
If `xForwardedHost` is `true`, it will use the `x-forwarded-host` header if it exists.
|
|
191
|
+
|
|
192
|
+
If no host header is found, it will return an empty string.
|
|
193
|
+
|
|
194
|
+
**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).
|
|
195
|
+
|
|
196
|
+
**Example:**
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
app.get("/", (event) => {
|
|
200
|
+
const host = getRequestHost(event); // "example.com"
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### `getRequestIP(event)`
|
|
205
|
+
|
|
206
|
+
Try to get the client IP address from the incoming request.
|
|
207
|
+
|
|
208
|
+
By default the address comes from `event.req.ip`: the connection peer, or the client resolved from the forwarded chain when the server is configured to trust an upstream proxy (e.g. srvx's `trustProxy`).
|
|
209
|
+
|
|
210
|
+
If `xForwardedFor` is `true`, the **first** entry of the `x-forwarded-for` header is returned instead, when the header exists.
|
|
211
|
+
|
|
212
|
+
If IP cannot be determined, it will default to `undefined`.
|
|
213
|
+
|
|
214
|
+
**Security:** `xForwardedFor` is opt-in because that first entry is client input. Proxies conventionally <u>append</u> to the chain (nginx `$proxy_add_x_forwarded_for`, most CDNs, and h3's own {@link proxy} util), so a value sent by the client stays at the left of the chain and is exactly what this returns — letting any caller choose their own address and defeat IP allow-lists, rate limiting, geo checks, and audit logs. Enabling it also <u>overrides</u> `event.req.ip`, discarding an address the server already resolved correctly. Prefer configuring the server to trust your proxy (srvx `trustProxy` walks the chain from the right, past trusted hops) and leave this option off; only enable it when an upstream you control always overwrites `x-forwarded-for` on every request.
|
|
215
|
+
|
|
216
|
+
**Example:**
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
app.get("/", (event) => {
|
|
220
|
+
const ip = getRequestIP(event); // "192.0.2.0"
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `getRequestProtocol(event, opts: { xForwardedProto? })`
|
|
225
|
+
|
|
226
|
+
Get the request protocol.
|
|
227
|
+
|
|
228
|
+
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.
|
|
229
|
+
|
|
230
|
+
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`).
|
|
231
|
+
|
|
232
|
+
If protocol cannot be determined, it will default to "http".
|
|
233
|
+
|
|
234
|
+
**Example:**
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
app.get("/", (event) => {
|
|
238
|
+
const protocol = getRequestProtocol(event); // "https"
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### `getRequestURL(event, opts: { xForwardedHost?, xForwardedProto? })`
|
|
243
|
+
|
|
244
|
+
Generated the full incoming request URL.
|
|
245
|
+
|
|
246
|
+
If `xForwardedHost` is `true`, it will use the `x-forwarded-host` header if it exists.
|
|
247
|
+
|
|
248
|
+
If `xForwardedProto` is `true`, it will use the `x-forwarded-proto` header if it exists.
|
|
249
|
+
|
|
250
|
+
**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).
|
|
251
|
+
|
|
252
|
+
**Example:**
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
app.get("/", (event) => {
|
|
256
|
+
const url = getRequestURL(event); // "https://example.com/path"
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### `getRouterParam(event, name, opts: { decode? })`
|
|
261
|
+
|
|
262
|
+
Get a matched route param by name.
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
**Example:**
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
app.get("/", (event) => {
|
|
270
|
+
const param = getRouterParam(event, "key");
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### `getRouterParams(event, opts: { decode? })`
|
|
275
|
+
|
|
276
|
+
Get matched route params.
|
|
277
|
+
|
|
278
|
+
By default params are returned exactly as they appeared in the URL path, still percent-encoded.
|
|
279
|
+
|
|
280
|
+
With `decode: true` each param is decoded **once** (like `decodeURIComponent`), except encoded path separators (`%2f`, `%5c`, at any `%25`-nesting depth) which are left in their encoded form so decoding can never reintroduce a `/` or `\` the router never matched.
|
|
281
|
+
|
|
282
|
+
A single decode is not the same as "fully decoded": `%25XX` decodes to the literal text `%XX`, so the result can still contain percent-escapes — including dot segments (`%252e%252e` -> `%2e%2e`) and control characters (`%2500` -> `%00`). **Do not decode the result again**: a second pass turns those back into traversal (`../`) and separators the routing and middleware layers never saw. Treat the returned string as final and validate it as-is.
|
|
283
|
+
|
|
284
|
+
**Example:**
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
app.get("/", (event) => {
|
|
288
|
+
const params = getRouterParams(event); // { key: "value" }
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Example:**
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// GET /files/%252e%252e/x
|
|
296
|
+
app.get("/files/**:rest", (event) => {
|
|
297
|
+
getRouterParams(event); // { rest: "%252e%252e/x" }
|
|
298
|
+
getRouterParams(event, { decode: true }); // { rest: "%2e%2e/x" } — still encoded, do not decode again
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### `getValidatedQuery(event, validate)`
|
|
303
|
+
|
|
304
|
+
Get the query param from the request URL validated with validate function.
|
|
305
|
+
|
|
306
|
+
You can use a simple function to validate the query object or use a Standard-Schema compatible library like `zod` to define a schema.
|
|
307
|
+
|
|
308
|
+
**Example:**
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
app.get("/", async (event) => {
|
|
312
|
+
const query = await getValidatedQuery(event, (data) => {
|
|
313
|
+
return "key" in data && typeof data.key === "string";
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Example:**
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
import { z } from "zod";
|
|
322
|
+
app.get("/", async (event) => {
|
|
323
|
+
const query = await getValidatedQuery(
|
|
324
|
+
event,
|
|
325
|
+
z.object({
|
|
326
|
+
key: z.string(),
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Example:**
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import * as v from "valibot";
|
|
336
|
+
app.get("/", async (event) => {
|
|
337
|
+
const params = await getValidatedQuery(
|
|
338
|
+
event,
|
|
339
|
+
v.object({
|
|
340
|
+
key: v.string(),
|
|
341
|
+
}),
|
|
342
|
+
{
|
|
343
|
+
onError: ({ issues }) => ({
|
|
344
|
+
statusText: "Custom validation error",
|
|
345
|
+
message: v.summarize(issues),
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### `getValidatedRouterParams(event, validate)`
|
|
353
|
+
|
|
354
|
+
Get matched route params and validate with validate function.
|
|
355
|
+
|
|
356
|
+
If `decode` option is `true`, params are decoded **once** exactly as described in {@link getRouterParams} — path separators stay encoded, other escapes decode a single level, and the validated value can still contain `%XX`. Validate it as-is; do not decode it again.
|
|
357
|
+
|
|
358
|
+
You can use a simple function to validate the params object or use a Standard-Schema compatible library like `zod` to define a schema.
|
|
359
|
+
|
|
360
|
+
**Example:**
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
app.get("/:key", async (event) => {
|
|
364
|
+
const params = await getValidatedRouterParams(event, (data) => {
|
|
365
|
+
return "key" in data && typeof data.key === "string";
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Example:**
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import { z } from "zod";
|
|
374
|
+
app.get("/:key", async (event) => {
|
|
375
|
+
const params = await getValidatedRouterParams(
|
|
376
|
+
event,
|
|
377
|
+
z.object({
|
|
378
|
+
key: z.string(),
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Example:**
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import * as v from "valibot";
|
|
388
|
+
app.get("/:key", async (event) => {
|
|
389
|
+
const params = await getValidatedRouterParams(
|
|
390
|
+
event,
|
|
391
|
+
v.object({
|
|
392
|
+
key: v.pipe(v.string(), v.picklist(["route-1", "route-2", "route-3"])),
|
|
393
|
+
}),
|
|
394
|
+
{
|
|
395
|
+
decode: true,
|
|
396
|
+
onError: ({ issues }) => ({
|
|
397
|
+
statusText: "Custom validation error",
|
|
398
|
+
message: v.summarize(issues),
|
|
399
|
+
}),
|
|
400
|
+
},
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### `isMethod(event, expected, allowHead?)`
|
|
406
|
+
|
|
407
|
+
Checks if the incoming request method is of the expected type.
|
|
408
|
+
|
|
409
|
+
If `allowHead` is `true`, it will allow `HEAD` requests to pass if the expected method is `GET`.
|
|
410
|
+
|
|
411
|
+
**Example:**
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
app.get("/", (event) => {
|
|
415
|
+
if (isMethod(event, "GET")) {
|
|
416
|
+
// Handle GET request
|
|
417
|
+
} else if (isMethod(event, ["POST", "PUT"])) {
|
|
418
|
+
// Handle POST or PUT request
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### `requestWithBaseURL(req, base, options: { url?: URL })`
|
|
424
|
+
|
|
425
|
+
Create a lightweight request proxy with the base path stripped from the URL pathname.
|
|
426
|
+
|
|
427
|
+
`options.url` is the parsed request URL to strip `base` from, in place of parsing `req.url`. Pass `event.url` whenever there is an event: for a non-canonical path it holds the canonicalized form the parent matched `base` against, while `req.url` still holds the wire form, and slicing one by an offset derived from the other is how mount prefixes desync.
|
|
428
|
+
|
|
429
|
+
### `requestWithURL(req, url)`
|
|
430
|
+
|
|
431
|
+
Create a lightweight request proxy that overrides only the URL.
|
|
432
|
+
|
|
433
|
+
Avoids cloning the original request (no `new Request()` allocation).
|
|
434
|
+
|
|
435
|
+
### `toRequest(input, options?)`
|
|
436
|
+
|
|
437
|
+
Convert input into a web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).
|
|
438
|
+
|
|
439
|
+
If input is a relative URL, it will be normalized into a full path based on the `host` header.
|
|
440
|
+
|
|
441
|
+
If input is already a Request and no options are provided, it will be returned as-is.
|
|
442
|
+
|
|
443
|
+
**Security:** The `host` header is client input. It is only used as the authority of the synthesized URL (falling back to `localhost` when absent or malformed) and can never widen into the path, and `x-forwarded-proto` is ignored, so the scheme is always `http`. Pass an absolute URL to control the origin.
|
|
444
|
+
|
|
445
|
+
### `getRequestFingerprint(event, opts)`
|
|
446
|
+
|
|
447
|
+
Get a unique fingerprint for the incoming request.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Response
|
|
2
|
+
|
|
3
|
+
> H3 response utilities.
|
|
4
|
+
|
|
5
|
+
## Event Stream
|
|
6
|
+
|
|
7
|
+
### `EventStream()`
|
|
8
|
+
|
|
9
|
+
### `isEventStream(input)`
|
|
10
|
+
|
|
11
|
+
## Sanitize
|
|
12
|
+
|
|
13
|
+
### `sanitizeStatusCode(statusCode?, defaultStatusCode)`
|
|
14
|
+
|
|
15
|
+
Make sure the status code is a valid HTTP status code.
|
|
16
|
+
|
|
17
|
+
### `sanitizeStatusMessage(statusMessage)`
|
|
18
|
+
|
|
19
|
+
Make sure the status message is safe to use in a response.
|
|
20
|
+
|
|
21
|
+
Allowed characters: horizontal tabs, spaces or visible ascii characters: [https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2](https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2)
|
|
22
|
+
|
|
23
|
+
## Serve Static
|
|
24
|
+
|
|
25
|
+
### `serveStatic(event, options)`
|
|
26
|
+
|
|
27
|
+
Dynamically serve static assets based on the request path.
|
|
28
|
+
|
|
29
|
+
**Security — path traversal:** `serveStatic` resolves `.`/`..` segments but deliberately keeps encoded separators (`%2f`, `%5c`) percent-encoded in the `id` it passes to `getMeta`/`getContents`, exactly as `event.url.pathname` does. The `id` therefore has the same segment structure the router and pathname-scoped `use()` guards matched on: `/private%5cx` stays one opaque segment and cannot be served as `/private/x` past a `use("/private/**")` guard. Resolve the `id` against your asset root as an opaque string — a backend that decodes it re-introduces separators and re-opens the hole.
|
|
30
|
+
|
|
31
|
+
A **non-canonical pathname is not served** (404, or falls through when `fallthrough` is set): more than one leading separator (`//private/x`, `/\\private/x`) or a dot segment that survived URL canonicalization, which means one spelled with `%25`-nested escapes (`/pub/%252e%252e/private/x`). Both dispatch to a catch-all route while missing a narrower `use("/private/**")` guard, and the only `id` `serveStatic` could build from them resolves back into the guarded path. Assets are reachable under their canonical spelling — the one routing and `use()` guards match on — only.
|
|
32
|
+
|
|
33
|
+
Everything else is decoded once for the on-disk lookup, so a file's real name reaches the backend: `/50%25.png` → `/50%.png`, `/a%20b` → `/a b`, and one `%25` level is peeled off a nested separator (`/a%252fb` → `/a%2fb`, still a literal `%2f`, never a boundary). RFC 3986's reserved set stays encoded, so an `id` can never grow a `?` or `#` that would truncate it in a URL.
|
|
34
|
+
|
|
35
|
+
Two things `serveStatic` cannot enforce for filesystem-backed assets: **case-insensitive filesystems** (macOS, Windows) need both sides of any allow/deny check case-folded (otherwise `/SECRET.env` slips past a check for `/secret.env`), and **symlinks** need the resolved path re-asserted against the asset root after following links (e.g. `realpath(target)`).
|
|
36
|
+
|
|
37
|
+
## More Response Utils
|
|
38
|
+
|
|
39
|
+
### `html(first)`
|
|
40
|
+
|
|
41
|
+
### `iterable(iterable)`
|
|
42
|
+
|
|
43
|
+
Iterate a source of chunks and send back each chunk in order. Supports mixing async work together with emitting chunks.
|
|
44
|
+
|
|
45
|
+
Each chunk must be a string or a buffer.
|
|
46
|
+
|
|
47
|
+
For generator (yielding) functions, the returned value is treated the same as yielded values.
|
|
48
|
+
|
|
49
|
+
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.)
|
|
50
|
+
|
|
51
|
+
**Example:**
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
return iterable(async function* work() {
|
|
55
|
+
// Open document body
|
|
56
|
+
yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n";
|
|
57
|
+
// Do work ...
|
|
58
|
+
for (let i = 0; i < 1000; i++) {
|
|
59
|
+
await delay(1000);
|
|
60
|
+
// Report progress
|
|
61
|
+
yield `<li>Completed job #`;
|
|
62
|
+
yield i;
|
|
63
|
+
yield `</li>\n`;
|
|
64
|
+
}
|
|
65
|
+
// Close out the report
|
|
66
|
+
return `</ol></body></html>`;
|
|
67
|
+
});
|
|
68
|
+
async function delay(ms) {
|
|
69
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `noContent(status)`
|
|
74
|
+
|
|
75
|
+
Respond with an empty payload.
|
|
76
|
+
|
|
77
|
+
**Example:**
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
app.get("/", () => noContent());
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `onDispose(event, cb)`
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
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`.
|
|
88
|
+
|
|
89
|
+
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`).
|
|
90
|
+
|
|
91
|
+
Note: this signals <u>"h3 is done with this event"</u>, not <u>"the client received the response"</u> — 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 <u>while still producing</u> the response (for example to abort an upstream fetch), use `event.req.signal` instead.
|
|
92
|
+
|
|
93
|
+
**Example:**
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
app.get("/sse", (event) => {
|
|
97
|
+
const interval = setInterval(() => {}, 1000);
|
|
98
|
+
onDispose(event, () => clearInterval(interval));
|
|
99
|
+
// ... return a streaming response
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `raw(value)`
|
|
104
|
+
|
|
105
|
+
Mark a string as trusted, pre-escaped HTML so it is used by the {@link html} util **without** being escaped.
|
|
106
|
+
|
|
107
|
+
Only use this for markup you fully control — passing user input to `raw` re-introduces XSS risk.
|
|
108
|
+
|
|
109
|
+
**Example:**
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// `heading` is trusted markup; `userName` is escaped automatically.
|
|
113
|
+
app.get("/", () => html`<div>${raw(heading)}<span>${userName}</span></div>`);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Example:**
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// Send a trusted markup string as-is:
|
|
120
|
+
app.get("/", () => html(raw("<h1>Hello, World!</h1>")));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `redirect(location, status, statusText?)`
|
|
124
|
+
|
|
125
|
+
Send a redirect response to the client.
|
|
126
|
+
|
|
127
|
+
It adds the `location` header to the response and sets the status code to 302 by default.
|
|
128
|
+
|
|
129
|
+
In the body, it sends a simple HTML page with a meta refresh tag to redirect the client in case the headers are ignored.
|
|
130
|
+
|
|
131
|
+
**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.
|
|
132
|
+
|
|
133
|
+
**Example:**
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
app.get("/", () => {
|
|
137
|
+
return redirect("https://example.com");
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Example:**
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
app.get("/", () => {
|
|
145
|
+
return redirect("https://example.com", 301); // Permanent redirect
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `redirectBack(event)`
|
|
150
|
+
|
|
151
|
+
Redirect the client back to the previous page using the `referer` header.
|
|
152
|
+
|
|
153
|
+
If the `referer` header is missing or is a different origin, it falls back to the provided URL (default `"/"`).
|
|
154
|
+
|
|
155
|
+
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.
|
|
156
|
+
|
|
157
|
+
**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.
|
|
158
|
+
|
|
159
|
+
**Example:**
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
app.post("/submit", (event) => {
|
|
163
|
+
// process form...
|
|
164
|
+
return redirectBack(event, { fallback: "/form" });
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `writeEarlyHints(event, hints)`
|
|
169
|
+
|
|
170
|
+
Write `HTTP/1.1 103 Early Hints` to the client.
|
|
171
|
+
|
|
172
|
+
In runtimes that don't support early hints natively, this function falls back to setting response headers which can be used by CDN.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Cookie
|
|
2
|
+
|
|
3
|
+
> H3 cookie utilities.
|
|
4
|
+
|
|
5
|
+
### `deleteChunkedCookie(event, name, serializeOptions?)`
|
|
6
|
+
|
|
7
|
+
Remove a set of chunked cookies by name.
|
|
8
|
+
|
|
9
|
+
### `deleteCookie(event, name, serializeOptions?)`
|
|
10
|
+
|
|
11
|
+
Remove a cookie by name.
|
|
12
|
+
|
|
13
|
+
### `getChunkedCookie(event, name)`
|
|
14
|
+
|
|
15
|
+
Get a chunked cookie value by name. Will join chunks together.
|
|
16
|
+
|
|
17
|
+
### `getCookie(event, name)`
|
|
18
|
+
|
|
19
|
+
Get a cookie value by name.
|
|
20
|
+
|
|
21
|
+
### `getValidatedCookies(event, validate, options?: { onError?: OnValidateError })`
|
|
22
|
+
|
|
23
|
+
### `parseCookies(event)`
|
|
24
|
+
|
|
25
|
+
Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs.
|
|
26
|
+
|
|
27
|
+
### `setChunkedCookie(event, name, value, options?)`
|
|
28
|
+
|
|
29
|
+
Set a cookie value by name. Chunked cookies will be created as needed.
|
|
30
|
+
|
|
31
|
+
### `setCookie(event, name, value, options?)`
|
|
32
|
+
|
|
33
|
+
Set a cookie value by name.
|