/commands/http/test-etag-if-none-match-304
Test ETag caching with curl: send If-None-Match and get 304 Not Modified
- tool
- HTTP
- versions
- curl 7.68+ (--etag-save, --etag-compare), RFC 9110
- os
- linux, macos, windows
To check that a server supports conditional requests, fetch the resource once, copy its ETag header, and send it back in If-None-Match: curl -i -H 'If-None-Match: "ETAG"' URL. A server that implements it answers 304 Not Modified with no body while the resource is unchanged, and 200 OK with the full body after it changes. curl 7.68 and later can do the bookkeeping with --etag-save FILE and --etag-compare FILE.
Try it against a local server
The server below computes a strong ETag from the body and honours If-None-Match:
cat > server.js <<'EOF'
const http = require("http");
const crypto = require("crypto");
const body = '{"items":[1,2,3]}\n';
const etag = '"' + crypto.createHash("sha1").update(body).digest("hex").slice(0, 12) + '"';
http.createServer((req, res) => {
res.setHeader("ETag", etag);
res.setHeader("Cache-Control", "no-cache");
if (req.headers["if-none-match"] === etag) { res.writeHead(304); return res.end(); }
res.setHeader("Content-Type", "application/json");
res.end(body);
}).listen(5207, "127.0.0.1");
setTimeout(() => process.exit(0), 10000);
EOF
node server.js &
sleep 1
etag=$(curl -s -I http://127.0.0.1:5207/items | tr -d '\r' | awk 'tolower($1)=="etag:" {print $2}')
echo "ETag: $etag"
echo "== matching ETag"
curl -s -i -H "If-None-Match: $etag" http://127.0.0.1:5207/items | tr -d '\r' | grep -iE '^(HTTP|etag|content-length)'
echo "== stale ETag"
curl -s -o /dev/null -w '%{http_code}\n' -H 'If-None-Match: "old"' http://127.0.0.1:5207/items
echo "== curl keeps the ETag in a file"
curl -s -o items.json --etag-save etag.txt http://127.0.0.1:5207/items
curl -s -o items.json -w '%{http_code}\n' --etag-compare etag.txt http://127.0.0.1:5207/items
waitOutput
ETag: "c2792215b282" == matching ETag HTTP/1.1 304 Not Modified ETag: "c2792215b282" == stale ETag 200 == curl keeps the ETag in a file 304
The first request with the matching ETag gets 304 and the same ETag header; a stale value gets the full 200 response. With --etag-compare, curl reads the saved value and sends it as If-None-Match for you.
Headers involved
| Header | Sent by | Meaning |
|---|---|---|
| ETag | Server | Opaque validator for the current version of the resource, in double quotes |
| If-None-Match | Client | Send the body only if the current ETag differs from these values (GET and HEAD answer 304) |
| Last-Modified | Server | Date of the last change, a weaker validator than ETag |
| If-Modified-Since | Client | Send the body only if it changed after this date; ignored when If-None-Match is present |
| Cache-Control: no-cache | Server | Clients may store the response but must revalidate it before each reuse |
Last-Modified instead of ETag
For servers that only send Last-Modified, use curl -z "date" URL or curl -z file URL: curl sends If-Modified-Since with that date (or the file's modification time), and a server that supports it returns 304 for unchanged content.
Pitfalls
- The quotes belong to the ETag.
If-None-Match: abcdoes not matchETag: "abc". - Compression can change the ETag: some servers append a suffix such as
-gzipor make the tag weak (W/) when they compress, so a value captured withoutAccept-Encodingmay not match a browser request. Cache-Control: no-storetells clients not to keep the response at all, so browsers will never sendIf-None-Match; see no-cache vs no-store.
FAQ
Why does my server always return 200 even with If-None-Match?
Either the value you send does not match the current ETag exactly (quotes and the W/ prefix are part of it), or the server or framework does not implement conditional GET for that route. Some proxies and compression layers also change or drop the ETag.
What is the difference between a strong and a weak ETag?
A weak ETag starts with W/ and only promises that the content is semantically equivalent. If-None-Match uses weak comparison, so W/"abc" matches "abc"; byte-range requests with If-Range need a strong ETag.
Does a 304 response have a body?
No. A 304 Not Modified response never carries content; the client reuses its cached copy and may update stored headers from the 304.