Version 1.4.1
July 20, 2026
Added
docs/reverse-proxy.mddocuments the Apache h2c trap. Withmod_http2enabled (the Debian/Ubuntu default shipsProtocols h2 h2c http/1.1), Apache answers plain HTTP withUpgrade: h2,h2c+Connection: Upgrade, which breaks nginx keepalive reuse: reused connections return200with an empty body. Small JSON responses fail most of the time while full pages mostly succeed, soedge/hydrate,edge/csrfand Craft's ownsession-infosilently return nothing and every form on a cached page stops working. Pin the backend vhost toProtocols http/1.1.
Changed
- The bundled example app now reflects current behaviour: templates use
{{ edgeCsrfInput() }},config/edge.phpsetscacheLoggedInRenders,config/general.phpcarries the correcttrustedHosts/ipHeaderspairing, and the nginx config gained the cache-key-aware bypass rule, a/actions/location and browser revalidation on cache hits.
Version 1.2.0
July 20, 2026
Added
{{ edgeCsrfInput() }}Twig function. Emits a token-free placeholder on a page that is being cached (the hydration runtime fills it per visitor) and the real token inline on a page that isn't — an excluded URI, a bypass, a signed-in render, an island fragment. A shared form partial no longer needs a flag threaded through it to say which kind of page it landed in, and the uncached case costs no round trip and needs no JavaScript.- The Edge Cache utility and
edge/nginx/verifynow warn whenqueryStringCachingisrespecton thenginx-staticdriver. That combination silently writes one file per query string that the tier can never look up, because it serves<host>/<uri>/index.htmlwith no place for a query segment.
Fixed
- The shipped nginx configs now bypass the cache for any request carrying a query param
that isn't in
excludedQueryStringParams. Without it,/shop?brand=xwas answered from the unfiltered/shopentry, sinceignoremode leaves the query string out of the key.tests/guards.phpasserts the map and the origin guard agree case by case. location @edgenow sendsCache-Control: public, max-age=0, must-revalidateon a hit. nginx serves the stored file verbatim, so the origin's headers never reached the visitor and browsers fell back to heuristic freshness — holding pages for an unpredictable window that no purge could reach.
Changed
Plugin::proxyWarnings()is nowPlugin::configWarnings()and covers driver/query-mode mismatches as well as trusted-proxy problems.
Version 1.1.0
July 20, 2026
Fixed
- A response rendered for a
Hostother than the site's configured base-URL host is no longer stored. The cache file is keyed by the site's own host while Craft renders absolute URLs from the requestHost, so a request arriving on another host (a bare IP, a forgedHost) could write its URLs into the canonical entry and have them served to every visitor. Such requests still render normally. - Cacheable responses now send
public, s-maxage=<cacheControlTtl>, max-age=0, must-revalidateinstead ofpublic, max-age=<cacheControlTtl>.max-ageis a browser directive, and a purge can never reach a browser: the previous header let a visitor hold stale HTML for the full TTL (a year, by default) with no way to recall it. - In
queryStringCaching: 'ignore'mode, a request carrying a query param that isn't inexcludedQueryStringParamsis no longer stored. The query string is dropped from the cache key in that mode, so/shop?brand=xwas being written over the entry for plain/shop. Marketing params are excluded by design and still cache. - CSRF hydration no longer races island hydration. The token is fetched once and applied
again to each island fragment after it swaps in, before
edge:islandfires, so forms inside islands are filled. - CSRF hydration now fills Craft's async
<craft-csrf-input>placeholder, which Formie also emits and whichasyncCsrfInputsproduces. Previously only plain inputs were filled.
Added
cacheLoggedInRenderssetting (defaultfalse). The edge tier already serves the shared copy to signed-in visitors, but their renders were never stored, so a page browsed only by signed-in staff never warmed. Enable it when the shell is identity-independent.- A response containing a rendered CSRF token (an input named after
csrfTokenNamewith a non-empty value) is refused and logged instead of being stored with a token baked in. - A response containing the signed-in user's email, username or full name is refused and
logged when
cacheLoggedInRendersis on. The matched field name is logged, never its value. This catches the obvious leak only; it cannot see group-scoped pricing or permission-scoped elements. X-Edge-Skip-Reasonheader on non-cacheable responses indevMode, naming the rule that matched.- The Edge Cache utility and
edge/nginx/verifywarn whentrustedHostscontainsanywhileipHeadersis set, which lets any client spoof its IP throughX-Forwarded-For. window.EdgeCsrf.ensure(root)/.apply(root)for filling CSRF fields in markup a site injects itself.docs/reverse-proxy.md, coveringtrustedHosts/ipHeadersfor both proxy topologies.- The shipped nginx configs gained a
location ^~ /actions/block, adefault_servercatch-all recipe, and a reverse-proxy variant for origins that aren't FastCGI. Without a.phphandler,try_files $uriresolves/index.phpto the file on disk and nginx serves the PHP source. tests/guards.php, a framework-free self-check for the pure guards (php tests/guards.php).
Changed
CacheDriverInterface::prepareResponse()takes an optional fourth argument,?string $skipReason. Drivers extendingBaseDriverneed no change; a driver implementing the interface directly must update its signature.
Version 1.0.0
July 17, 2026
- Initial release.