Repository: caddyserver/caddy (language: go)

Issue:
ETag headers explicitly set in configuration should be honored rather than overridden
The mtime+size pattern used to calculate default etags by Caddy is only useful in situations where mtime is accurate. This isn't always the case: Some build systems (notably [Nix](https://nixos.org/)) coerce all times to 1970-01-01 for better reproducibility.

Right now, however, any etag that might be set by other configuration or modules is overwritten by the default mtime+size etag when caddy is serving requests. This makes Caddy deeply unreliable for serving files built with Nix, as it inaccurately instructs client browsers that contents are unchanged whenever subsequent builds have identical file sizes.

If the customer deploying caddy has a better identifier (such as the overall hash of the filesystem tree being served), they should be able to configure an ETag header using that identifier and let it be honored.


Discussion / hints:
I see from your commit that it looks like you propose that if the Etag header is already set on the response, to use that, and only set an Etag if not present.

I think that seems reasonable. Want to make a PR?

PS. How _are_ you setting the Etag header yourself?
> I think that seems reasonable. Want to make a PR?

Incoming momentarily!

> PS. How _are_ you setting the Etag header yourself?

I'm using Nix to generate my Caddy config file, just as I'm using it to generate the content being served. All content built by Nix has a hash included in its file location (`/nix/store/<hash>-<name>-<version>/...`), so one could extract the hash from the path, or hash the name of the directory itself, or do several other such things.

Fix patch:
diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go
index 2b5cc3de39f..feef66de834 100644
--- a/modules/caddyhttp/fileserver/staticfiles.go
+++ b/modules/caddyhttp/fileserver/staticfiles.go
@@ -356,7 +356,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 	}
 
 	var file fs.File
-	var etag string
+
+	// etag is usually unset, but if the user knows what they're doing, let them override it
+	etag := w.Header().Get("Etag")
 
 	// check for precompressed files
 	for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) {
@@ -388,7 +390,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 		// don't assign info = compressedInfo because sidecars are kind
 		// of transparent; however we do need to set the Etag:
 		// https://caddy.community/t/gzipped-sidecar-file-wrong-same-etag/16793
-		etag = calculateEtag(compressedInfo)
+		if etag == "" {
+			etag = calculateEtag(compressedInfo)
+		}
 
 		break
 	}
@@ -408,7 +412,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 		}
 		defer file.Close()
 
-		etag = calculateEtag(info)
+		if etag == "" {
+			etag = calculateEtag(info)
+		}
 	}
 
 	// at this point, we're serving a file; Go std lib supports only
@@ -421,7 +427,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 
 	// set the Etag - note that a conditional If-None-Match request is handled
 	// by http.ServeContent below, which checks against this Etag value
-	w.Header().Set("Etag", etag)
+	if etag != "" {
+		w.Header().Set("Etag", etag)
+	}
 
 	if w.Header().Get("Content-Type") == "" {
 		mtyp := mime.TypeByExtension(filepath.Ext(filename))


Test patch: