Response

H3 response utilities.

Event Stream

createEventStream(event, opts?)

Initialize an EventStream instance for creating server sent events

Example:

import { createEventStream } from "h3";

app.get("/sse", (event) => {
  const eventStream = createEventStream(event);

  // Send a message every second
  const interval = setInterval(async () => {
    await eventStream.push("Hello world");
  }, 1000);

  // cleanup the interval and close the stream when the connection is terminated
  eventStream.onClosed(async () => {
    console.log("closing SSE...");
    clearInterval(interval);
    await eventStream.close();
  });

  return eventStream.send();
});

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 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.

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.