UNPKG

h3

Version:

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

169 lines (107 loc) 6.82 kB
# Response > H3 response utilities. ## Event Stream ### `EventStream()` ### `isEventStream(input)` ## Sanitize ### `sanitizeStatusCode(statusCode?, defaultStatusCode)` Make sure the status code is a valid HTTP status code. ### `sanitizeStatusMessage(statusMessage)` Make sure the status message is safe to use in a response. Allowed characters: horizontal tabs, spaces or visible ascii characters: [https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2](https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2) ## Serve Static ### `serveStatic(event, options)` Dynamically serve static assets based on the request path. **Security — path traversal:** `serveStatic` resolves `.`/`..` segments and normalizes the request path, but deliberately keeps encoded separators **percent-encoded** in the `id` it passes to `getMeta`/`getContents`: `%2f` (encoded `/`) always survives, and a double-encoded backslash arrives as a literal `%5c` (a single-encoded `%5c` is decoded to `\` and normalized away). Traversal safety therefore depends on those backends **not** decoding the `id`: a backend that percent-decodes it (e.g. an extra `decodeURIComponent`, or a lookup layer that decodes) re-introduces separators and **re-opens the traversal hole**. Resolve the `id` against your asset root as an opaque string. When implementing custom `getMeta`/`getContents` over a real filesystem, the integrator is also responsible for two things `serveStatic` cannot enforce. **Case-insensitive filesystems** (macOS, Windows): case-fold both sides of any allow/deny checks — otherwise `/SECRET.env` can slip past a check written for `/secret.env`. **Symlink containment:** re-assert that the resolved path stays within the asset root after following links (e.g. compare `realpath(target)` against the root), since a symlink can point outside it. ## More Response Utils ### `html(first)` ### `iterable(iterable)` Iterate a source of chunks and send back each chunk in order. Supports mixing async work together with emitting chunks. Each chunk must be a string or a buffer. For generator (yielding) functions, the returned value is treated the same as yielded values. The first chunk is awaited before the response is created, so status and headers staged while producing it (`event.res.status`, `event.res.headers`) are still applied. Everything set after the first chunk is ignored — headers are already on the wire by then. (Returning a raw `ReadableStream` gives no such window: its response is created before the stream is read.) **Example:** ```ts return iterable(async function* work() { // Open document body yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n"; // Do work ... for (let i = 0; i < 1000; i++) { await delay(1000); // Report progress yield `<li>Completed job #`; yield i; yield `</li>\n`; } // Close out the report return `</ol></body></html>`; }); async function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } ``` ### `noContent(status)` Respond with an empty payload. **Example:** ```ts app.get("/", () => noContent()); ``` ### `onDispose(event, cb)` Register a callback that runs once the event is fully over: the response body finished streaming, the client disconnected, or the body errored — on every runtime, not just Node.js. The callback receives `undefined` on normal completion, or the cancel/abort reason otherwise. Callbacks run in registration order after the global `onResponse` hook; sync throws and async rejections are absorbed (reported via `console.error` unless the app is configured with `silent`), and pending async callbacks are passed to `waitUntil`. Registering after disposal invokes the callback immediately. Registration is only guaranteed to observe the end of the event when made during request handling (handler, middleware, or `onResponse`). Note: this signals <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. **Example:** ```ts app.get("/sse", (event) => { const interval = setInterval(() => {}, 1000); onDispose(event, () => clearInterval(interval)); // ... return a streaming response }); ``` ### `raw(value)` Mark a string as trusted, pre-escaped HTML so it is used by the {@link html} util **without** being escaped. Only use this for markup you fully control — passing user input to `raw` re-introduces XSS risk. **Example:** ```ts // `heading` is trusted markup; `userName` is escaped automatically. app.get("/", () => html`<div>${raw(heading)}<span>${userName}</span></div>`); ``` **Example:** ```ts // Send a trusted markup string as-is: app.get("/", () => html(raw("<h1>Hello, World!</h1>"))); ``` ### `redirect(location, status, statusText?)` Send a redirect response to the client. It adds the `location` header to the response and sets the status code to 302 by default. In the body, it sends a simple HTML page with a meta refresh tag to redirect the client in case the headers are ignored. **Security:** If `location` derives from user input (query params, form fields, headers, etc.), validate it against an allow-list of permitted destinations before redirecting. Passing user-controlled values through unchecked creates an open redirect vulnerability. Prefer `redirectBack` for "return to previous page" flows, which only honors same-origin referers. **Example:** ```ts app.get("/", () => { return redirect("https://example.com"); }); ``` **Example:** ```ts app.get("/", () => { return redirect("https://example.com", 301); // Permanent redirect }); ``` ### `redirectBack(event)` Redirect the client back to the previous page using the `referer` header. If the `referer` header is missing or is a different origin, it falls back to the provided URL (default `"/"`). By default, only the **pathname** of the referer is used (query string and hash are stripped) to prevent spoofed referers from carrying unintended parameters. Set `allowQuery: true` to preserve the query string. **Security:** The `fallback` value MUST be a trusted, hardcoded path — never use user input. Passing user-controlled values (e.g., query params) as `fallback` creates an open redirect vulnerability. **Example:** ```ts app.post("/submit", (event) => { // process form... return redirectBack(event, { fallback: "/form" }); }); ``` ### `writeEarlyHints(event, hints)` Write `HTTP/1.1 103 Early Hints` to the client. In runtimes that don't support early hints natively, this function falls back to setting response headers which can be used by CDN.