vcl 4.1; import vsthrottle; import std; backend app { .host = "app"; .port = "${APP_PORT}"; } acl purge { "127.0.0.1"; "::1"; } sub vcl_recv { # Internal markers can only be set by our own VCL. unset req.http.X-RFC9211-State; unset req.http.X-RFC9211-Entry; unset req.http.X-RFC9211-TTL; unset req.http.X-RFC9211-Synthetic; unset req.http.Cache-Status; if (vsthrottle.is_denied(client.identity, 200, 10s, 60s)) { return (synth(429, "Too Many Requests")); } if (req.method == "PURGE") { if (!client.ip ~ purge) { return (synth(405, "Not allowed")); } return (purge); } if (!req.http.X-Forwarded-Proto) { set req.http.X-Forwarded-Proto = "https"; } if (req.url ~ "^/healthcheck(\?.*)?$" || req.http.X-Internal-Check) { return (pass); } # Only WebSocket upgrades use pipe, not Socket.IO polling. if (req.http.Upgrade ~ "(?i)websocket") { return (pipe); } if (req.url ~ "^/socket\.io/") { return (pass); } if (req.method != "GET" && req.method != "HEAD") { return (pass); } if (req.http.Authorization) { return (pass); } if (req.http.Accept-Encoding) { if (req.http.Accept-Encoding ~ "zstd") { set req.http.Accept-Encoding = "zstd"; } else if (req.http.Accept-Encoding ~ "br") { set req.http.Accept-Encoding = "br"; } else if (req.http.Accept-Encoding ~ "gzip") { set req.http.Accept-Encoding = "gzip"; } else { set req.http.Accept-Encoding = "identity"; } } # Public static resources do not vary by session cookie. if (req.url ~ "^/(static|uploads)/" || req.url ~ "(?i)\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { unset req.http.Cookie; return (hash); } return (hash); } sub vcl_pipe { if (req.http.Upgrade) { set bereq.http.Upgrade = req.http.Upgrade; set bereq.http.Connection = "Upgrade"; } } sub vcl_hash { hash_data(req.url); if (req.http.host) { hash_data(req.http.host); } else { hash_data(server.ip); } if (req.http.Cookie) { hash_data(req.http.Cookie); } if (req.http.Accept-Encoding) { hash_data(req.http.Accept-Encoding); } if (req.http.X-Accept-Image) { hash_data(req.http.X-Accept-Image); } } # Track actual request transitions; leave the final return to builtin VCL. sub vcl_hit { set req.http.X-RFC9211-State = "hit"; } sub vcl_miss { # 'miss' is intentionally generic: we cannot always distinguish # uri-miss from vary-miss in this VCL hook. set req.http.X-RFC9211-State = "miss"; } sub vcl_pass { if (req.method == "GET" || req.method == "HEAD") { set req.http.X-RFC9211-State = "bypass"; } else { set req.http.X-RFC9211-State = "method"; } } sub vcl_backend_fetch { # Never forward diagnostic state to the application. unset bereq.http.X-RFC9211-State; unset bereq.http.X-RFC9211-Entry; unset bereq.http.X-RFC9211-TTL; unset bereq.http.X-RFC9211-Synthetic; } sub vcl_backend_response { # Origin must not control our local backend-error marker. unset beresp.http.X-RFC9211-Synthetic; # Never store backend 5xx responses. if (beresp.status >= 500) { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } if (beresp.http.Cache-Control ~ "(?i)no-store|private|no-cache" || beresp.http.Surrogate-Control ~ "(?i)no-store" || beresp.http.Vary == "*") { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } if (beresp.status >= 300 && beresp.status < 400) { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } if (bereq.url ~ "^/(static|uploads)/" || bereq.url ~ "(?i)\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { if (beresp.status != 200) { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } } if (bereq.url ~ "(?i)\.(js|css)(\?.*)?$" && beresp.http.Content-Type ~ "(?i)text/html") { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } if (bereq.url ~ "(?i)\.js(\?.*)?$" && !beresp.http.Content-Type) { set beresp.http.Content-Type = "application/javascript; charset=utf-8"; } if (bereq.url ~ "(?i)\.css(\?.*)?$" && !beresp.http.Content-Type) { set beresp.http.Content-Type = "text/css; charset=utf-8"; } if (bereq.url ~ "^/(static|uploads)/" || bereq.url ~ "(?i)\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { unset beresp.http.Set-Cookie; if (beresp.http.Vary) { set beresp.http.Vary = regsuball(beresp.http.Vary, "(?i)(^|,)[[:space:]]*Cookie[[:space:]]*(,|$)", "\1"); set beresp.http.Vary = regsuball(beresp.http.Vary, ",[[:space:]]*,", ","); set beresp.http.Vary = regsub(beresp.http.Vary, "^[[:space:]]*,[[:space:]]*", ""); set beresp.http.Vary = regsub(beresp.http.Vary, "[[:space:]]*,[[:space:]]*$", ""); if (beresp.http.Vary ~ "^[[:space:]]*$") { unset beresp.http.Vary; } } if (!(beresp.http.Cache-Control ~ "(?i)(s-maxage|max-age)")) { set beresp.ttl = 24h; set beresp.http.Cache-Control = "public, max-age=86400, immutable"; } set beresp.grace = 1h; set beresp.keep = 24h; } if (beresp.http.Cache-Control ~ "(?i)s-maxage=([0-9]+)") { set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*s-maxage=([0-9]+).*", "\1") + "s", 0s); } else if (beresp.http.Cache-Control ~ "(?i)max-age=([0-9]+)") { set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*max-age=([0-9]+).*", "\1") + "s", 0s); } else if (beresp.http.Expires) { set beresp.ttl = std.time(beresp.http.Expires, now) - now; if (beresp.ttl < 0s) { set beresp.ttl = 0s; } } else if (beresp.ttl <= 0s) { set beresp.ttl = 60s; } if (beresp.http.Cache-Control ~ "(?i)immutable") { set beresp.grace = 1h; set beresp.keep = 24h; } if (!beresp.http.Content-Encoding && bereq.http.Accept-Encoding ~ "gzip") { if (beresp.http.Content-Type ~ "(?i)text/|application/(javascript|json|xml)") { set beresp.do_gzip = true; } } if (beresp.http.Content-Length && std.integer(beresp.http.Content-Length, 0) > 1048576) { set beresp.do_stream = true; } } sub vcl_backend_error { # Synthetic fetch errors were not served from cache. set beresp.uncacheable = true; set beresp.ttl = 0s; set beresp.http.X-RFC9211-Synthetic = "1"; return (deliver); } sub vcl_deliver { # Cache-Status MUST be calculated per request, not cached as an origin header. # Synthetic backend errors do not receive a local Cache-Status member. if (resp.http.X-RFC9211-Synthetic) { unset resp.http.Cache-Status; } else { if (req.http.X-RFC9211-State == "hit") { # VCL DURATION -> STRING produces seconds with 3 decimal places. # RFC 9211 ttl is an integer; negative subsecond TTL is rounded to -1. set req.http.X-RFC9211-TTL = obj.ttl; if (obj.ttl < 0s && obj.ttl > -1s) { set req.http.X-RFC9211-TTL = "-1"; } else { set req.http.X-RFC9211-TTL = regsub(req.http.X-RFC9211-TTL, "\.[0-9]+$", ""); } set req.http.X-RFC9211-Entry = "varnish; hit; ttl=" + req.http.X-RFC9211-TTL; } else if (req.http.X-RFC9211-State == "miss") { set req.http.X-RFC9211-Entry = "varnish; fwd=miss"; if (obj.uncacheable) { set req.http.X-RFC9211-Entry = req.http.X-RFC9211-Entry + "; stored=?0"; } else { set req.http.X-RFC9211-Entry = req.http.X-RFC9211-Entry + "; stored"; } } else if (req.http.X-RFC9211-State == "method") { set req.http.X-RFC9211-Entry = "varnish; fwd=method; stored=?0"; } else if (req.http.X-RFC9211-State == "bypass") { set req.http.X-RFC9211-Entry = "varnish; fwd=bypass; stored=?0"; } # Preserve the existing upstream cache chain, append Varnish LAST. # If the request path is unknown, avoid fabricating a bypass. if (req.http.X-RFC9211-Entry) { if (resp.http.Cache-Status) { set resp.http.Cache-Status = resp.http.Cache-Status + ", " + req.http.X-RFC9211-Entry; } else { set resp.http.Cache-Status = req.http.X-RFC9211-Entry; } } } unset resp.http.X-RFC9211-Synthetic; unset resp.http.X-Cache; unset resp.http.X-Pass-Reason; unset resp.http.Via; unset resp.http.X-Varnish; unset resp.http.Server; unset resp.http.Content-Disposition; # Preserve Age (it is standard HTTP cache metadata). } sub vcl_synth { set resp.http.Cache-Control = "private, no-store, no-cache"; unset resp.http.Cache-Status; unset resp.http.X-Cache; unset resp.http.X-Pass-Reason; unset resp.http.Via; unset resp.http.X-Varnish; unset resp.http.Server; unset resp.http.Content-Disposition; } sub vcl_purge { return (synth(200, "Purged")); }