When implementing HTTP caching strategies, many developers encounter a puzzling scenario: despite matching Last-Modified
and If-Modified-Since
headers, Apache sometimes returns 200 OK
instead of the expected 304 Not Modified
. Let's dissect why this happens and how to achieve predictable caching behavior.
In HTTP caching, several headers work together:
Request Headers:
If-Modified-Since: [date]
If-None-Match: [ETag]
Response Headers:
Last-Modified: [date]
ETag: [entity tag]
Cache-Control: [directives]
The complete validation process considers both modification dates and ETags. When browsers send both If-Modified-Since
and If-None-Match
, the server must validate against both.
Your setup reveals several important points:
Cache-Control: No-cache
If-Modified-Since: Tue, 14 Oct 2014 15:10:37 GMT
Last-Modified: Tue, 14 Oct 2014 15:10:37 GMT
ETag: "1461-505636af08fcd-gzip"
The presence of Cache-Control: No-cache
actually means "revalidate with server before using cached version," not "don't cache." However, the real culprit is often ETag validation.
Apache's default behavior considers both modification time and ETag. Even if dates match, ETag validation might fail because:
- File size changes (even with same content)
- Inode numbers differ in cluster environments
- Gzip compression alters the entity
Your observation about ETags confirms this:
# Apache configuration to disable ETags
Header unset ETag
FileETag None
For consistent 304 responses, implement these configurations:
Option 1: Disable ETags Completely
<IfModule mod_headers.c>
Header unset ETag
</IfModule>
FileETag None
Option 2: Simplified ETag Generation
FileETag MTime Size
Option 3: Fine-tuned Cache-Control
<FilesMatch "\.(html|css|js)$">
Header set Cache-Control "max-age=0, must-revalidate"
</FilesMatch>
Test your configuration with:
curl -I http://yoursite.com/resource
curl -H "If-Modified-Since: [date]" -H "If-None-Match: [etag]" -I http://yoursite.com/resource
In load-balanced environments, ensure consistent file metadata across servers:
# In apache.conf
FileETag INode MTime Size
Remember that different servers might have different inodes for the same file, causing ETag mismatches.
Add this to your VirtualHost to track 304 responses:
CustomLog logs/access.log "%h %l %u %t \"%r\" %>s %b\"%{Referer}i\" \"%{User-Agent}i\"" env=!dontlog
SetEnvIf Request_Protocol "HTTP/1.1" is_http_1_1
SetEnvIf status 304 dontlog
This helps monitor caching behavior without cluttering logs with successful 304s.
When working with HTTP caching, we expect servers to return 304 Not Modified responses when both conditions are met:
- The
If-Modified-Since
header matches the resource'sLast-Modified
date - The
If-None-Match
header matches the currentETag
However, as shown in the example request/response headers:
Request Headers:
If-Modified-Since: Tue, 14 Oct 2014 15:10:37 GMT
If-None-Match: "1461-505636af08fcd-gzip"
Response Headers:
Last-Modified: Tue, 14 Oct 2014 15:10:37 GMT
ETag: "1461-505636af08fcd-gzip"
Status Code: 200 OK
Apache's behavior stems from these technical realities:
- Cache-Control: no-cache takes precedence over validation headers
- ETag comparison happens before Last-Modified validation
- Gzip encoding creates complications with ETag matching
Here are three approaches to achieve consistent 304 responses:
Option 1: Disable ETags (Recommended)
# In Apache configuration
Header unset ETag
FileETag None
Option 2: Configure Cache-Control Properly
<FilesMatch "\.(html|css|js)$">
Header set Cache-Control "max-age=0, must-revalidate"
</FilesMatch>
Option 3: Force Weak ETag Validation
# In .htaccess
FileETag MTime Size
Header edit ETag ^"(.+)"$ "W/\"$1\""
Verify the solution works using cURL:
curl -I -H "If-Modified-Since: [last_modified_date]" \
-H "If-None-Match: [etag_value]" [your_url]
The proper response should now show:
HTTP/1.1 304 Not Modified