vcl 4.1; # # dotnews-paas base VCL — installed to /etc/varnish/default.vcl by bootstrap.sh. # The two included files are GENERATED by `paas` from the app registry — never # edit them by hand: # /srv/paas/vcl/paas-backends.vcl (probe + backend per cached app) # /srv/paas/vcl/paas-routing.vcl (Host -> backend + per-app pass rules) # # Policy summary: # - only GET/HEAD are ever cached; POST always passes; other methods -> 405 # - TTL/grace come from the app: Cache-Control s-maxage / stale-while-revalidate # (no header -> default_ttl 120s micro-cache; cached apps are opt-in) # - 5xx never enters the cache; stale copies are served instead when present # - 404 cached max 60s # - tracking params stripped from the cache key; remaining query sorted # - cookies stripped on cacheable paths (no app reads cookies during SSR) # - purge: lurker-friendly BAN per Host, localhost only # - browsers never see s-maxage; edge-cached responses go out as no-cache import std; include "/srv/paas/vcl/paas-backends.vcl"; acl purgers { "127.0.0.1"; "::1"; } sub vcl_recv { # ---- cache purge (from paas CLI / app publish hooks on this box) ---- if (req.method == "BAN") { if (client.ip !~ purgers) { return (synth(403, "Forbidden")); } if (!req.http.X-Ban-Host) { return (synth(400, "X-Ban-Host header required")); } if (req.http.X-Ban-Path) { # URL-scoped ban (optional, generic): only objects of this host whose # URL matches the regex. obj.*-only expression -> lurker-friendly. ban("obj.http.X-Host == " + req.http.X-Ban-Host + " && obj.http.X-Url ~ " + req.http.X-Ban-Path); return (synth(200, "Banned " + req.http.X-Ban-Host + " paths " + req.http.X-Ban-Path)); } ban("obj.http.X-Host == " + req.http.X-Ban-Host); return (synth(200, "Banned " + req.http.X-Ban-Host)); } if (req.method != "GET" && req.method != "HEAD" && req.method != "POST") { return (synth(405, "Method Not Allowed")); } # ---- Host -> backend routing + per-app pass rules (generated) ---- include "/srv/paas/vcl/paas-routing.vcl"; # POSTs are never served from cache and never create cache entries # (the Vercel bug class: cached responses served to POSTs). if (req.method == "POST") { return (pass); } # ---- cache-key normalization: drop tracking params, sort the rest ---- set req.url = regsuball(req.url, "([?&])(utm_[a-zA-Z0-9_]+|gclid|gbraid|wbraid|dclid|gclsrc|fbclid|msclkid|ttclid|twclid|li_fat_id|mc_cid|mc_eid|igshid|_hsenc|_hsmi|pk_campaign|pk_kwd)=[^&]*", "\1"); set req.url = regsuball(req.url, "&&+", "&"); set req.url = regsub(req.url, "\?&", "?"); set req.url = regsub(req.url, "[?&]+$", ""); set req.url = std.querysort(req.url); # No app behind this cache reads cookies during SSR (audited) — strip them # so stray consent/analytics cookies can't fragment or bypass the cache. unset req.http.Cookie; # Short grace when healthy (stale-while-revalidate window on top of the # app-provided one), long grace when the backend is down (serve-stale). if (std.healthy(req.backend_hint)) { set req.grace = 1h; } else { set req.grace = 168h; } return (hash); } sub vcl_backend_response { # Ban targets for lurker-friendly bans (matched in the BAN expressions above). set beresp.http.X-Host = bereq.http.host; set beresp.http.X-Url = bereq.url; # 5xx must NEVER enter the cache. If this was a background refresh of a # stale object, abandon so the last good copy keeps being served # (sitemaps/"last good copy" behavior). Foreground with no stale copy: # deliver the error uncacheable, hit-for-miss 2m so requests don't pile up. if (beresp.status >= 500) { if (bereq.is_bgfetch) { return (abandon); } set beresp.uncacheable = true; set beresp.ttl = 2m; return (deliver); } if (beresp.status == 404) { set beresp.ttl = 60s; set beresp.grace = 0s; return (deliver); } # Defense in depth: anything setting cookies is not shared-cacheable. if (beresp.http.Set-Cookie) { set beresp.uncacheable = true; set beresp.ttl = 2m; return (deliver); } # TTL comes from s-maxage, grace from stale-while-revalidate (native in # Varnish >= 6). Give every cacheable object a disaster-grace floor so a # dead backend keeps being served from stale for up to 72h. if (beresp.ttl > 0s && beresp.grace < 72h) { set beresp.grace = 72h; } # Store text compressed once; Varnish auto-gunzips for legacy clients. if (beresp.http.Content-Type ~ "text|application/json|application/xml|application/javascript|image/svg") { set beresp.do_gzip = true; } } sub vcl_deliver { unset resp.http.X-Host; unset resp.http.X-Url; if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; set resp.http.X-Cache-Hits = obj.hits; } else { set resp.http.X-Cache = "MISS"; } # Edge directives are for Varnish only. Responses the app marked with # s-maxage go to browsers as no-cache (SEO sites: always revalidate — # Varnish answers those revalidations in <1ms). Responses without s-maxage # (e.g. no-store token pages) keep their headers untouched. if (resp.http.Cache-Control ~ "s-maxage") { set resp.http.Cache-Control = "no-cache"; } unset resp.http.Via; unset resp.http.X-Varnish; unset resp.http.Server; }