Skip to content
Ilha
Esc
↑↓navigate↵open⌘Jpreview
On this page

Middleware and security

Security headers, the deny-by-default frame guard, trusted origins, CSRF, and the production hardening model.

@ilha/router hardens the server-island endpoint by default. This page is the full guard and origin model behind Server islands.

Security headers

Both respond() and the low-level httpResponse() emit sensible security headers by default:

Header Value
Content-Type text/html
X-Content-Type-Options nosniff
Referrer-Policy no-referrer
Cache-Control no-store

A header is added only when the caller has not already set it, so you can override any default by passing headers.

// Default: nosniff, no-referrer, no-store (see table above).
const const res: Responseres = function httpResponse(body: string | null, options?: HttpResponseOptions): Response
Build an HTTP `Response` for SSR output with sensible security headers: `Content-Type: text/html`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `Cache-Control: no-store`, and an optional CSP. This is a low-level helper — prefer {@link RouterBuilder.respond } for the full render+head+headers pipeline.
httpResponse
("<h1>Hi</h1>");
// Conservative default CSP via a nonce — pass the same nonce to <script nonce=…>. const const withCsp: ResponsewithCsp = function httpResponse(body: string | null, options?: HttpResponseOptions): Response
Build an HTTP `Response` for SSR output with sensible security headers: `Content-Type: text/html`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `Cache-Control: no-store`, and an optional CSP. This is a low-level helper — prefer {@link RouterBuilder.respond } for the full render+head+headers pipeline.
httpResponse
(const html: "<h1>Hi</h1>"html, { HttpResponseOptions.cspNonce?: string | undefined
CSP nonce for inline scripts. When set (and `contentSecurityPolicy` is not), a conservative default CSP is emitted with `'nonce-${nonce}'` for `script-src` — pass the same nonce to head `<script nonce=…>` tags.
cspNonce
: const nonce: "abc123"nonce });
// Full CSP string wins over the nonce-derived default. const const locked: Responselocked = function httpResponse(body: string | null, options?: HttpResponseOptions): Response
Build an HTTP `Response` for SSR output with sensible security headers: `Content-Type: text/html`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `Cache-Control: no-store`, and an optional CSP. This is a low-level helper — prefer {@link RouterBuilder.respond } for the full render+head+headers pipeline.
httpResponse
(const html: "<h1>Hi</h1>"html, {
HttpResponseOptions.contentSecurityPolicy?: string | undefined
Full `Content-Security-Policy` string; overrides the nonce-derived default.
contentSecurityPolicy
: "default-src 'self'",
});
Option Behavior
status Response status (default 200).
headers Extra headers, merged over defaults.
cspNonce Emit a conservative default CSP with 'nonce-${nonce}' for script-src.
contentSecurityPolicy Full CSP string; overrides the nonce-derived default.

Prefer respond() when you are rendering a route — it runs the full render + head + headers pipeline. Reach for httpResponse() when you already have the HTML and only need a correctly-headered Response.

Deny-by-default frame endpoint

In production, the internal frame endpoint is denied unless you install a guard:

Endpoint Serves Method Default when unguarded
/__ilha/frame Re-render of a server island or server page POST Denied (403)

An unguarded frame endpoint would let anyone re-render any server island by id — if an island’s state holds private data you must install a session check. The dev middleware stays permissive unless you register a guard, so local iteration works out of the box.

Installing guards

Register guards once at server startup for production, via the shared registry:

// Gates /__ilha/frame in production; without this the endpoint returns 403.
function setFrameGuard(guard: FrameGuard): void
Install a guard consulted by every `/__ilha/frame` request (dev middleware and the production `@ilha/router/ssr` handler share this slot — both read it from `globalThis`). Return a `Response` to reject; return nothing to allow. Island state is world-readable through frames unless you gate them, so apps serving private data should install a session check here.
setFrameGuard
((request: Requestrequest) => {
if (!function isSignedIn(_request: Request): booleanisSignedIn(request: Requestrequest)) return new var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response
The **`Response`** interface of the Fetch API represents the response to a request. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)
Response
("Unauthorized", { ResponseInit.status?: number | undefinedstatus: 401 });
}); // Optional: explicit trusted origins and a CSRF verifier for the frame POST. function setFrameAuth(policy: FrameAuthPolicy): void
Install the frame-authorization policy consumed by the production `@ilha/router/ssr` handler. `trustedOrigins` and `csrf` are also applied by the dev middleware (via `IlhaPagesOptions`).
setFrameAuth
({
FrameAuthPolicy.trustedOrigins?: string[] | undefined
Explicit trusted origins (e.g. `"https://app.example.com"`). When set, origin checks accept only these; otherwise the check compares the `Origin` header against `https://{host}` / `http://{host}`.
trustedOrigins
: ["https://app.example.com"],
// Verify a per-session token issued by your server (double-submit cookie or // synchronizer token), compared in constant time. Never use a build-time // environment variable or literal string — anything shipped to the browser // is public and cannot be a CSRF secret. FrameAuthPolicy.csrf?: ((request: Request) => boolean | Promise<boolean>) | undefined
Optional CSRF verifier for the state-changing frame POST. Receives the original `Request`; returning falsy rejects the request. Use this for server-to-server frame callers that have no browser `Origin`.
csrf
: (request: Requestrequest) => request: Requestrequest.Request.headers: Headers
The **`headers`** read-only property of the Request interface contains the Headers object associated with the request. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers)
headers
.Headers.get(name: string): string | null
The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a Headers object with a given name. If the requested header doesn't exist in the Headers object, it returns null. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get)
get
("x-csrf") === const expectedToken: string | undefinedexpectedToken,
});

In dev, configure the guard through the plugin options instead — pass frameGuard to the pages plugin.

setFrameAuth

Tuning the frame-auth policy:

Option Type Behavior
defaultAction "open" | "deny" "deny" is the default. "open" opts out of deny-by-default (not recommended).
trustedOrigins string[] Explicit origins to accept for frame origin checks. Otherwise the Origin header is compared against the request’s own Host.
csrf (request) => boolean Extra verifier for the frame POST — covers non-browser callers that send no Origin header.

What is forwarded

The middleware forwards only cookie, authorization, and user-agent headers to the scoped frame render. It deliberately does not forward a client-supplied x-forwarded-for header — it is spoofable and must not be trusted for IP checks. If you relied on it for IP-based decisions, move that check to a trusted proxy layer instead.

Validation and status clamping

  • Frame bodies are streamed with a hard 16 KiB cap; oversized bodies return 413.
  • Frame paths are validated and fail closed — an invalid path returns 400 instead of falling back to /.
  • Failures are typed: the frame render returns a FrameError result, and redirect() coerces invalid statuses to 302; error() coerces invalid statuses to 500 (silently).
  • Render failures surface as "frame failed" — error details are never leaked to clients in production.
Topic Guide
Server islands and frames Server islands
Error handling Error boundaries
Guards via the pages plugin File-system routing
Production wiring Deployment

Was this page helpful?