summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPer Buer <per.buer@gmail.com>2026-09-10 20:40:40 +0200
committerPer Buer <per.buer@gmail.com>2026-09-10 20:40:40 +0200
commit87f57024729ac293fd7c93727f1acbb21e6505c0 (patch)
tree17cc5db44179d336a4a3481d389ff63a83f3e368
parentbf39010df8328f89ccc53f4ea6c9b6549d71eade (diff)
contrib/varnish: example VCL for ESI diff fragments
Turns on ESI processing for frames carrying Surrogate-Control, hashes esi-diff fragments on their query string so that every fork sharing a commit shares one cached rendering, and refuses fragment requests that do not come from the ESI processor so they do not become one more thing for crawlers to enumerate. Frames get a short TTL on purpose: they are cheap to render and there are a lot of them, and they should not crowd fragments out of the cache. cgit-esi.vtc exercises the VCL against a fake cgit backend, asking for the same commit through two repository names and checking that the backend saw exactly one fragment request. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-rw-r--r--contrib/varnish/README18
-rw-r--r--contrib/varnish/cgit-esi.vcl63
-rw-r--r--contrib/varnish/cgit-esi.vtc64
3 files changed, 145 insertions, 0 deletions
diff --git a/contrib/varnish/README b/contrib/varnish/README
new file mode 100644
index 0000000..68d713a
--- /dev/null
+++ b/contrib/varnish/README
@@ -0,0 +1,18 @@
+Varnish in front of cgit with ESI diff fragments
+================================================
+
+Set "enable-esi=1" in cgitrc (see cgitrc(5), section EDGE SIDE INCLUDES),
+then include cgit-esi.vcl from your VCL:
+
+ vcl 4.1;
+ backend cgit { .host = "127.0.0.1"; .port = "8080"; }
+ include "/etc/varnish/cgit-esi.vcl";
+
+The included file turns on ESI processing for the commit and diff frames,
+hashes the esi-diff fragments on their query string so that all forks share
+one rendering per commit, and refuses fragment requests that do not come
+from the ESI processor.
+
+cgit-esi.vtc exercises the VCL with a fake cgit backend:
+
+ varnishtest -D cgit_vcl=$PWD/cgit-esi.vcl cgit-esi.vtc
diff --git a/contrib/varnish/cgit-esi.vcl b/contrib/varnish/cgit-esi.vcl
new file mode 100644
index 0000000..d3b54b0
--- /dev/null
+++ b/contrib/varnish/cgit-esi.vcl
@@ -0,0 +1,63 @@
+# Varnish configuration for cgit's ESI diff fragments (cgitrc: enable-esi=1).
+#
+# This file is meant to be included from your own VCL after the "vcl 4.1;"
+# line and the backend definition, for example:
+#
+# vcl 4.1;
+# backend cgit { .host = "127.0.0.1"; .port = "8080"; }
+# include "cgit-esi.vcl";
+#
+# With enable-esi=1 the commit and diff pages come back from cgit as a cheap
+# repository-specific frame carrying a Surrogate-Control header and an
+# <esi:include> of /<repo>/esi-diff/?id=... The fragment is the expensive
+# part (the rendered diff) and its output does not depend on the repository
+# or branch it was requested through, so it is hashed on its query string
+# alone. Every fork holding the same commit then shares one cached
+# rendering.
+
+sub vcl_recv {
+ # Frames follow the built-in VCL: a request carrying a cookie (an
+ # Anubis token, say) is passed to cgit, but ESI is still processed on
+ # the way out and the fragments below are cached regardless.
+ if (req.url ~ "/esi-diff/") {
+ # Fragments are only ever fetched by the ESI processor; do not
+ # let them become yet another crawlable URL.
+ if (req.esi_level == 0) {
+ return (synth(403, "Forbidden"));
+ }
+ # Nothing in the fragment depends on the client.
+ unset req.http.Cookie;
+ unset req.http.Authorization;
+ return (hash);
+ }
+}
+
+sub vcl_hash {
+ if (req.esi_level > 0 && req.url ~ "/esi-diff/") {
+ # Strip the repository path: same objects, same output.
+ hash_data(regsub(req.url, "^.*/esi-diff/", "/esi-diff/"));
+ return (lookup);
+ }
+}
+
+sub vcl_backend_response {
+ if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
+ unset beresp.http.Surrogate-Control;
+ set beresp.do_esi = true;
+ set beresp.do_gzip = true;
+ # The frame is per repository and branch and cheap to render.
+ # cgit asks for ten years; keep it short so frames do not
+ # crowd fragments out of the cache.
+ set beresp.ttl = 5m;
+ set beresp.grace = 1h;
+ } else if (bereq.url ~ "/esi-diff/") {
+ set beresp.do_gzip = true;
+ if (beresp.status == 200) {
+ # Content addressed by object id never changes.
+ set beresp.ttl = 30d;
+ set beresp.grace = 30d;
+ } else {
+ set beresp.ttl = 30s;
+ }
+ }
+}
diff --git a/contrib/varnish/cgit-esi.vtc b/contrib/varnish/cgit-esi.vtc
new file mode 100644
index 0000000..671d6a1
--- /dev/null
+++ b/contrib/varnish/cgit-esi.vtc
@@ -0,0 +1,64 @@
+vtest "cgit ESI diff fragments are shared across repositories"
+
+# Run with: varnishtest -D cgit_vcl=$PWD/cgit-esi.vcl cgit-esi.vtc
+
+server s1 {
+ # Frame for the commit as seen through fork a.git
+ rxreq
+ expect req.url == "/a.git/commit/?id=abc"
+ txresp -hdr {Surrogate-Control: content="ESI/1.0"} \
+ -body {<html><head><base href='/a.git/'/></head><body>A:<esi:include src='/a.git/esi-diff/?id=abc&id2=def&dt=0'/>:Z</body></html>}
+
+ # The fragment, fetched exactly once
+ rxreq
+ expect req.url == "/a.git/esi-diff/?id=abc&id2=def&dt=0"
+ expect req.http.Cookie == <undef>
+ txresp -body {<div class='diff'>DIFF</div>}
+
+ # Frame for the same commit through fork b.git; no fragment fetch follows
+ rxreq
+ expect req.url == "/b.git/commit/?id=abc&h=next"
+ txresp -hdr {Surrogate-Control: content="ESI/1.0"} \
+ -body {<html><head><base href='/b.git/'/></head><body>B:<esi:include src='/b.git/esi-diff/?id=abc&id2=def&dt=0'/>:Z</body></html>}
+
+ # A different diff option is a different fragment
+ rxreq
+ expect req.url == "/b.git/commit/?id=abc&dt=1"
+ txresp -hdr {Surrogate-Control: content="ESI/1.0"} \
+ -body {<html><body>B:<esi:include src='/b.git/esi-diff/?id=abc&id2=def&dt=1'/>:Z</body></html>}
+ rxreq
+ expect req.url == "/b.git/esi-diff/?id=abc&id2=def&dt=1"
+ txresp -body {<table class='ssdiff'>SSDIFF</table>}
+} -start
+
+varnish v1 -vcl+backend {
+ include "${cgit_vcl}";
+} -start
+
+client c1 {
+ txreq -url "/a.git/commit/?id=abc" -hdr "Cookie: session=1"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.Surrogate-Control == <undef>
+ expect resp.body == "<html><head><base href='/a.git/'/></head><body>A:<div class='diff'>DIFF</div>:Z</body></html>"
+
+ txreq -url "/b.git/commit/?id=abc&h=next"
+ rxresp
+ expect resp.status == 200
+ expect resp.body == "<html><head><base href='/b.git/'/></head><body>B:<div class='diff'>DIFF</div>:Z</body></html>"
+
+ txreq -url "/b.git/commit/?id=abc&dt=1"
+ rxresp
+ expect resp.status == 200
+ expect resp.body == "<html><body>B:<table class='ssdiff'>SSDIFF</table>:Z</body></html>"
+
+ # Fragments cannot be fetched from the outside
+ txreq -url "/a.git/esi-diff/?id=abc&id2=def&dt=0"
+ rxresp
+ expect resp.status == 403
+} -run
+
+# Two frames, two distinct fragments: four backend fetches in total, i.e.
+# the second fork did not re-render the diff.
+varnish v1 -expect n_object == 4
+varnish v1 -expect cache_hit == 1