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
#Serve Static
#serveStatic(event, options)
Dynamically serve static assets based on the request path.
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.
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.
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.
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)).
#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:
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:
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 "h3 is done with this event", not "the client received the response" — 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 while still producing the response (for example to abort an upstream fetch), use event.req.signal instead.
Example:
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:
// `heading` is trusted markup; `userName` is escaped automatically.
app.get("/", () => html`<div>${raw(heading)}<span>${userName}</span></div>`);Example:
// 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:
app.get("/", () => {
return redirect("https://example.com");
});Example:
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:
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.