[master] 2c68ba0db cache_req_fsm: keep the cache object's Content-Length for HEAD always

Nils Goroll nils.goroll at uplex.de
Wed Sep 3 13:50:05 UTC 2025


commit 2c68ba0dbe9f9674a655a230a4677355ce91385c
Author: Nils Goroll <nils.goroll at uplex.de>
Date:   Thu Jan 2 16:27:13 2025 +0100

    cache_req_fsm: keep the cache object's Content-Length for HEAD always
    
    Previously, we would only keep the Content-Length header for HEAD requests on
    hit-for-miss objects, now we simply keep it always to enable "fallback" caching
    of HEAD requests.
    
    The added vtc implements the basics of the logic to enable the (reasonable) use
    case documented in
    https://github.com/varnishcache/varnish-cache/issues/2107#issuecomment-2536642262
    but using Vary instead of cache key modification plus restart.
    
    Fixes #4245

diff --git a/bin/varnishd/cache/cache_req_fsm.c b/bin/varnishd/cache/cache_req_fsm.c
index 50d726845..9dc0db092 100644
--- a/bin/varnishd/cache/cache_req_fsm.c
+++ b/bin/varnishd/cache/cache_req_fsm.c
@@ -484,15 +484,12 @@ cnt_transmit(struct worker *wrk, struct req *req)
 			http_Unset(req->resp, H_Content_Length);
 		} else if (clval >= 0 && clval == req->resp_len) {
 			/* Reuse C-L header */
-		} else if (head && req->objcore->flags & OC_F_HFM) {
-			/*
-			 * Don't touch C-L header (debatable)
-			 *
-			 * The only way to do it correctly would be to GET
-			 * to the backend, and discard the body once the
-			 * filters have had a chance to chew on it, but that
-			 * would negate the "pass for huge objects" use case.
+		} else if (head) {
+			/* rfc9110,l,3226,3227
+			 * "MAY send Content-Length ... [for] HEAD"
+			 * do not touch to support cached HEAD #4245
 			 */
+			req->resp_len = 0;
 		} else {
 			http_Unset(req->resp, H_Content_Length);
 			if (req->resp_len >= 0)
diff --git a/bin/varnishtest/tests/r04245.vtc b/bin/varnishtest/tests/r04245.vtc
new file mode 100644
index 000000000..f6c1b7142
--- /dev/null
+++ b/bin/varnishtest/tests/r04245.vtc
@@ -0,0 +1,82 @@
+varnishtest "cache a HEAD as a fallback for a GET - Content-Length preserved for cached HEAD"
+
+server s1 {
+	rxreq
+	expect req.method == "HEAD"
+	expect req.http.t == "headmiss"
+	txresp -nolen -hdr "Content-Length: 42"
+
+	rxreq
+	expect req.method == "GET"
+	expect req.http.t == "getmiss"
+	txresp -bodylen 42
+} -start
+
+varnish v1 -vcl+backend {
+    sub vcl_recv {
+	if (req.method == "HEAD") {
+	    set req.http.X-Fetch-Method = "HEAD";
+	} else {
+	    unset req.http.X-Fetch-Method;
+	}
+    }
+
+    sub vcl_backend_fetch {
+	if (bereq.http.X-Fetch-Method) {
+	    set bereq.method = bereq.http.X-Fetch-Method;
+	}
+    }
+
+    sub vcl_backend_response {
+	# NOTE: this use of Vary is specific to this case, it is
+	# usually WRONG to only set Vary for a specific condition
+	if (bereq.http.X-Fetch-Method) {
+	    if (beresp.http.Vary) {
+		set beresp.http.Vary += ", X-Fetch-Method";
+	    } else {
+		set beresp.http.Vary = "X-Fetch-Method";
+	    }
+	}
+	set beresp.http.t = bereq.http.t;
+    }
+
+    sub vcl_deliver {
+	# Vary cleanup
+	if (resp.http.Vary == "X-Fetch-Method") {
+	    unset resp.http.Vary;
+	} else if (resp.http.Vary ~ ", X-Fetch-Method$") {
+	    set resp.http.Vary =
+		regsub(resp.http.Vary, ", X-Fetch-Method$", "");
+	}
+    }
+} -start
+
+client c1 {
+	# miss
+	txreq -method "HEAD" -hdr "t: headmiss"
+	rxresphdrs
+	expect resp.http.t == "headmiss"
+	expect resp.http.Content-Length == 42
+	# hit
+	txreq -method "HEAD" -hdr "t: headhit"
+	rxresphdrs
+	expect resp.http.t == "headmiss"
+	expect resp.http.Content-Length == 42
+
+	# miss
+	txreq -hdr "t: getmiss"
+	rxresp
+	expect resp.http.t == "getmiss"
+	expect resp.http.Content-Length == 42
+	# hits on full object
+	txreq -hdr "t: gethit"
+	rxresp
+	expect resp.http.t == "getmiss"
+	expect resp.http.Content-Length == 42
+	txreq -method "HEAD" -hdr "t: getheadhit"
+	rxresphdrs
+	expect resp.http.t == "getmiss"
+	expect resp.http.Content-Length == 42
+} -run
+
+server s1 -wait


More information about the varnish-commit mailing list