Skip to content

[file_server] Caddy is not verifying etags from etag filesΒ #7096

@Pizmovc

Description

@Pizmovc

Some background
I'm using NixOS as my server OS with Caddy serving my blog and other sites. I've set it up so that it is serving the files directly from the Nix store. What is important to know here is that when building with Nix it strips all timestamps from files, so no modification times are present (found a somewhat related issue). So since Caddy can't use modification times to respond to If-Modified-Since header, since files are seemingly never modified, it always responds with 304 Not Modified, breaking caching for my blog. So far Caddy is behaving as expected.

So I've modified my Caddyfile to force Caddy to use ETags with the following config:

luka.korosec.cc {
	header {
		-Last-Modified
	}
	file_server {
		etag_file_extensions .etag
	}
	root * /nix/store/3gsfndbnp4v4z0k7cs12b82vqvvpqxr7-tech-blog-1.0.0
	encode gzip
}

I've removed the Last-Modified header (so browsers can't use If-Modified-Since header), and added etag_file_extensions. I generate the ETag files during Nix build and Caddy correctly uses them as etag header. Up till now, I can't fault Caddy for anything, but what follows is a Caddy bug I think.

The bug
Here is what I think shows a bug:

  1. Make initial request: curl -v https://luka.korosec.cc
  2. Note the ETag in response: etag: b79e32c68bbcbbf5599c452748e903f6
  3. Make conditional request: curl -v -H "If-None-Match: b79e32c68bbcbbf5599c452748e903f6" https://luka.korosec.cc
  4. Observe that server returns 200 OK instead of 304 Not Modified
curl session
❯ curl -v https://luka.korosec.cc
[...]
> GET / HTTP/2
> Host: luka.korosec.cc
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
< accept-ranges: bytes
< alt-svc: h3=":443"; ma=2592000
< content-type: text/html; charset=utf-8
< etag: b79e32c68bbcbbf5599c452748e903f6
< server: Caddy
< vary: Accept-Encoding
< content-length: 1485
< date: Fri, 27 Jun 2025 19:07:19 GMT
<
<!DOCTYPE html>
[...]

❯ curl -v -H "If-None-Match: b79e32c68bbcbbf5599c452748e903f6" https://luka.korosec.cc
[...]
> GET / HTTP/2
> Host: luka.korosec.cc
> User-Agent: curl/8.7.1
> Accept: */*
> If-None-Match: b79e32c68bbcbbf5599c452748e903f6
>
* Request completely sent off
< HTTP/2 200
< accept-ranges: bytes
< alt-svc: h3=":443"; ma=2592000
< content-type: text/html; charset=utf-8
< etag: b79e32c68bbcbbf5599c452748e903f6
< server: Caddy
< vary: Accept-Encoding
< content-length: 1485
< date: Fri, 27 Jun 2025 19:09:07 GMT
<
<!DOCTYPE html>
[...]
root directory files
❯ ls -lah /nix/store/3gsfndbnp4v4z0k7cs12b82vqvvpqxr7-tech-blog-1.0.0
dr-xr-xr-x root root    82 B  1970-01-01 01:00:01 πŸ“‚ ./
drwxrwxr-t root nixbld  11 MB 2025-06-27 05:08:40 πŸ“‚ ../
dr-xr-xr-x root root   164 B  1970-01-01 01:00:01 πŸ“‚ css/
dr-xr-xr-x root root   462 B  1970-01-01 01:00:01 πŸ“‚ fonts/
dr-xr-xr-x root root   172 B  1970-01-01 01:00:01 πŸ“‚ img/
dr-xr-xr-x root root     8 B  1970-01-01 01:00:01 πŸ“‚ posts/
.r--r--r-- root root   1.5 KB 1970-01-01 01:00:01 πŸ“„ index.html
.r--r--r-- root root    33 B  1970-01-01 01:00:01 πŸ“„ index.html.etag

❯ cat /nix/store/3gsfndbnp4v4z0k7cs12b82vqvvpqxr7-tech-blog-1.0.0/index.html.etag
b79e32c68bbcbbf5599c452748e903f6

What should happen
When the If-None-Match header matches the current ETag, the server should return 304 Not Modified with no response body.

I'm running Caddy version 2.10.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions