Skip to content
Ilha
Esc
navigateopen⌘Jpreview
On this page

Middleware and security

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

@ilha/router hardens SSR and server-island endpoints by default. This page is the full guard and origin model. It sits behind Server rendering and 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.

import { httpResponse } from "@ilha/router";
const html = "<h1>Hi</h1>";
const nonce = "abc123";
// ---cut---
// Default: nosniff, no-referrer, no-store (see table above).
const res = httpResponse("<h1>Hi</h1>");

// Conservative default CSP via a nonce — pass the same nonce to <script nonce=…>.
const withCsp = httpResponse(html, { cspNonce: nonce });

// Full CSP string wins over the nonce-derived default.
const locked = httpResponse(html, {
  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 endpoints

In production, two internal endpoints are 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)
/__ilha/loader Loader data for a regular page navigation GET Denied (403)

An unguarded /__ilha/frame would let anyone re-render any server island by id — if an island’s state holds private data you must install a session check. /__ilha/loader is gated the same way through the shared policy. 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:

import {
  setFrameAuth,
  setFrameGuard,
  setLoaderGuard,
} from "@ilha/router/server-island-registry";

function isSignedIn(_request: Request) {
  return true;
}
// ---cut---
// Gates /__ilha/frame in production; without this the endpoint returns 403.
setFrameGuard((request) => {
  if (!isSignedIn(request))
    return new Response("Unauthorized", { status: 401 });
});

// Gates /__ilha/loader independently (falls back to the frame guard when absent).
setLoaderGuard((request) =>
  isSignedIn(request)
    ? undefined
    : new Response("Unauthorized", { status: 401 }),
);

// Optional: explicit trusted origins and a CSRF verifier for the frame POST.
setFrameAuth({
  trustedOrigins: ["https://app.example.com"],
  csrf: (request) =>
    request.headers.get("x-csrf") === process.env.CSRF_SECRET,
});

In dev, configure the guards through the plugin options instead — see Plugin options.

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

The same defaultAction policy gates both the loader and the frame.

What is forwarded

The middleware forwards only cookie, authorization, and user-agent headers to loaders and frames. 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.
  • Frame paths are validated and fail closed — an invalid path returns 400 instead of falling back to /.
  • redirect() coerces invalid statuses to 302; error() coerces invalid statuses to 500 (silently).
  • Non-LoaderError loader failures surface as "Internal error" outside dev — error details are never leaked to clients in production.
Topic Guide
Server islands and frames Server islands
Rendering routes to a Response Server rendering
Guards via the pages plugin File-system routing — plugin options
Production wiring Deployment

Was this page helpful?