Cache-Control Headers Set But Browser Always Gets 200 Status

1. The problem I’m having:

I’ve configured Cache-Control headers in my Caddyfile for static assets (images, CSS, JS) but browsers always receive 200 status instead of 304 Not Modified for cached resources. The Cache-Control headers are being sent correctly in responses, and ETags are present, but conditional requests (If-None-Match) don’t seem to work properly. Resources are always re-downloaded instead of being served from cache with 304 responses except document.

2. Error messages and/or full log output:

Expected: HTTP/1.1 304 Not Modified in the second refresh but only document got 304!

3. Caddy version:

v2.10.0

4. How I installed and ran Caddy:

Using brew.

a. System environment:

Mac OS Catalina

b. Command:

caddy file-server --root ./dist --domain 192.168.1.8 --listen :9000

d. My complete Caddy config:

{
	debug
}

localhost {
	root * ./dist
	
	file_server
	
	@images {
		path *.png *.jpg *.jpeg *.gif *.webp *.avif *.ico *.svg
	}
	
	@static_assets {
		path *.css *.js
	}
	
	@astro_assets {
		path /_astro/*
	}
	
	header @astro_assets {
		Cache-Control "public, max-age=31536000, immutable"
	}
	
	header @images {
		Cache-Control "public, max-age=2592000"
	}
	
	header @static_assets {
		Cache-Control "public, max-age=604800"
	}
	
	header {
		X-Content-Type-Options "nosniff"
		X-Frame-Options "DENY"
		X-XSS-Protection "1; mode=block"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}
	
	encode gzip
	try_files {path} {path}/ /index.html
}

You’re not running the Caddyfile you think you’re running.

I did reload with Caddy file config I have in the current directory!

But the file-server command doesn’t react to the reload command. You have to run Caddy properly to take up the correct configuration.

I think the docs’ section below exist in my project!

--config is the path to the config file. If omitted, assumes Caddyfile in current directory if it exists; otherwise, this flag is required.

This flag doesn’t go with file-server command nor is it in your post. Can you share in details how you run Caddy?

1 Like

I run the cmd below with this config.

caddy start                      
2025/07/11 13:44:41.264	INFO	maxprocs: Leaving GOMAXPROCS=4: CPU quota undefined
2025/07/11 13:44:41.265	INFO	GOMEMLIMIT is updated	{"package": "github.com/KimMachineGun/automemlimit/memlimit", "GOMEMLIMIT": 15461882265, "previous": 9223372036854775807}
2025/07/11 13:44:41.265	INFO	using adjacent Caddyfile
2025/07/11 13:44:41.268	INFO	adapted config to JSON	{"adapter": "caddyfile"}
2025/07/11 13:44:41.286	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2025/07/11 13:44:41.287	INFO	http.auto_https	enabling automatic HTTP->HTTPS redirects	{"server_name": "srv0"}
2025/07/11 13:44:41.287	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc00060c400"}
2025/07/11 13:44:41.304	INFO	http	enabling HTTP/3 listener	{"addr": ":9000"}
2025/07/11 13:44:41.304	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2025/07/11 13:44:41.304	WARN	http	HTTP/2 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2025/07/11 13:44:41.304	WARN	http	HTTP/3 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2025/07/11 13:44:41.304	INFO	http.log	server running	{"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2025/07/11 13:44:41.304	INFO	http	enabling automatic TLS certificate management	{"domains": ["192.168.1.8"]}
2025/07/11 13:44:41.319	INFO	tls	storage cleaning happened too recently; skipping for now	{"storage": "FileStorage:/Users/wpplumber/Library/Application Support/Caddy", "instance": "93df3002-60ff-4bab-9f6c-73ead327a123", "try_again": "2025/07/12 13:44:41.319", "try_again_in": 86399.999998999}
2025/07/11 13:44:41.329	INFO	tls	finished cleaning storage units
2025/07/11 13:44:41.380	INFO	pki.ca.local	root certificate is already trusted by system	{"path": "storage:pki/authorities/local/root.crt"}
2025/07/11 13:44:41.382	INFO	autosaved config (load with --resume flag)	{"file": "/Users/wpplumber/Library/Application Support/Caddy/autosave.json"}
2025/07/11 13:44:41.382	INFO	serving initial configuration
Successfully started Caddy (pid=62959) - Caddy is running in the background

192.168.1.8:9000 {
	root * ./dist

	# Cache headers
	@images {
		file
		path *.png *.jpg *.jpeg *.gif *.webp *.avif *.ico *.svg
	}

	@static_assets {
		file
		path *.css *.js
	}

	@astro_assets {
		file
		path /_astro/*
	}

	@fonts {
		file
		path *.woff *.woff2 *.ttf *.eot
	}

	# Cache control headers
	header @astro_assets {
		Cache-Control "public, max-age=31536000, immutable"
	}

	header @images {
		Cache-Control "public, max-age=2592000"
	}

	header @static_assets {
		Cache-Control "public, max-age=604800"
	}

	header @fonts {
		Cache-Control "public, max-age=31536000"
	}

	# Security headers
	header {
		X-Content-Type-Options "nosniff"
		X-Frame-Options "DENY"
		X-XSS-Protection "1; mode=block"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}

	# Enable compression
	encode gzip

	# File server configuration
	file_server

	# SPA fallback
	try_files {path} {path}/ /index.html
}

Oh I see it. Look, the document itself was served with 304, so Caddy respected the etag and the header. The 200s you see for the following resources aren’t from Caddy. Notice how the duration is 0ms. They are cached by the browser. Because the document is 304-ed, the sub-reseurces aren’t requested by the browser. If you add the transferred column, you’ll see the browser saying “cached”.

2 Likes

After more tests I found that resources cached by browser as mentioned!

I come to this result:

:white_check_mark: Caching is working perfectly!
:white_check_mark: The browser saw Cache-Control: public, max-age=604800 and skipped the network request entirely (since the file was still fresh).

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.