Skip to main content

HTTP Status Code Propagation

PRISM captures and propagates HTTP status codes from the origin through the rendered response. For SPAs that handle routing client-side, PRISM also supports status code signaling from the application itself.

Default Behavior​

PRISM captures the HTTP status code from Chrome's navigation response to the origin URL. If the origin returns a 404, PRISM's response to the bot is also a 404. If the status code cannot be captured (e.g., timeout), it defaults to 200.

SPA Status Override​

SPAs typically return HTTP 200 for every route because the server delivers the same index.html shell regardless of the path. Client-side routing then renders the appropriate content. This means a SPA's "Not Found" page still returns a 200 status, which confuses search engines.

Enable status_from_meta to let your SPA signal the correct status code:

[render]
status_from_meta = true

PRISM checks three sources in priority order:

1. JavaScript Global (Highest Priority)​

Set window.__PRISM_STATUS__ to a numeric HTTP status code in your SPA:

// In your 404 route component
window.__PRISM_STATUS__ = 404;

PRISM evaluates this after rendering completes. It takes precedence over all other signals.

2. Meta Tag​

Add a meta tag to the rendered <head>:

<meta name="render:status_code" content="404">

or the alternate form:

<meta name="prism:status" content="404">

Both tag names are supported. With React Helmet, this is straightforward:

import { Helmet } from 'react-helmet';

function NotFoundPage() {
return (
<>
<Helmet>
<title>Page Not Found</title>
<meta name="render:status_code" content="404" />
</Helmet>
<h1>404 - Page Not Found</h1>
</>
);
}

3. Navigation Response (Lowest Priority)​

The HTTP status code captured from Chrome's navigation to the origin URL. This is the default fallback when no SPA signals are present.

Common Status Code Use Cases​

StatusUse Case
200Normal page
301Permanent redirect (signal via meta tag for SPA soft redirects)
404Page not found
410Permanently removed content
503Temporary maintenance page

How long a failure is kept​

A response that reports a failure describes the moment it was produced, not the page. PRISM keeps those on their own short clock, so a problem that lasts a second does not become an outage that lasts an hour.

5xx from the origin​

When the final status code is 500 or higher, PRISM stores the response under cache.error_ttl_secs rather than the route's TTL:

[cache]
error_ttl_secs = 60 # keep a 5xx for 60 seconds

The default is 30 seconds. Set it to 0 to skip storing 5xx responses altogether.

Since 1.5.3 the Cache-Control PRISM sends downstream carries this same number, so a CDN or browser in front holds the error for exactly as long as PRISM does. Before that it announced the route's TTL — with the shipped defaults, an error kept for 30 seconds was advertised for 3600, and anything caching in front went on serving it long after the origin recovered. When error_ttl_secs is 0, the response is announced no-store: PRISM will not keep it, so it does not ask anyone else to.

Which statuses are stored at all​

RFC 9111 §3 names the statuses a cache may store on its own judgement. Everything else needs the origin to state a lifetime:

Stored under the route's TTLNeeds Cache-Control from the origin
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501302, 303, 307, 400, 401, 403, 407, 429, 451, and anything else

The distinction that matters for a storefront is between the two kinds of redirect. 301 and 308 are permanent and are stored. 302, 303 and 307 say temporary — and until 1.6.0 PRISM kept them for the full route lifetime, so a page moved aside for maintenance, an experiment or a geography stayed moved for crawlers long after it came back. They are now stored only if the origin asks:

Cache-Control: max-age=300

Server errors are the exception, and they answer to cache.error_ttl_secs instead — that setting is the operator's explicit permission, which is why 5xx does not pass through this rule.

401, 403, 407 and 429 from the origin​

These four say something about this request — it was not authorised, it asked too often — rather than about the page, so since 1.5.3 PRISM does not store them on the strength of its own configured TTL. RFC 9111 §3 does not list them among the statuses a cache may store heuristically.

If your origin does want one of them cached, it has to say so:

Cache-Control: max-age=60

PRISM then honours that lifetime and caps it exactly as it caps any other — the origin's grant limits PRISM's TTL and never extends it. Without such a header the response is served and forgotten.

Requests carrying credentials never reach the cache at all while cache.skip_authenticated is on, which it is by default; the rule above covers the unauthenticated request that happens to receive one of these.

A write invalidates what was rendered​

RFC 9111 §4.4. When a POST, PUT, PATCH or DELETE comes back from the origin with a non-error status, the page it addressed has changed, so every cached copy of that URL is dropped — including the mobile and Vary variants, which an exact-key delete would leave behind still serving the old page.

Before 1.6.0 nothing did this: the write was not renderable, so it bypassed straight to the origin and the cache was never consulted. A crawler kept receiving the version rendered before the change until the TTL expired.

Two limits are worth knowing. Safety is a property of the method — GET, HEAD, OPTIONS and TRACE are safe and never invalidate; everything else does, including methods PRISM has never heard of. And this is local: a write landing on one replica leaves the others holding their own copies. Closing that needs durable purge propagation, which is 1.7 work.

PRISM's own errors​

When PRISM itself answers — a rate limit it applied, a queue it could not admit to, an origin it could not reach — the response carries no-store. A cache holding PRISM's 429 would go on refusing clients PRISM would already serve, and PRISM would never see those requests to know it was happening.