Request header
> If-None-Match: "33a64df5"If-None-Match Header
Learn how the If-None-Match header makes conditional requests using ETags. Avoid downloading unchanged resources and reduce bandwidth with cache validation.
- Direction
- Request
- Category
- Conditional requests
- Spec
- RFC 9110 §13.1.2
- JS can set it
- Yes
- CORS-safelisted
- No: may trigger a preflight
On this page
TL;DR:
If-None-Matchsends back the ETag you already have and asks the server to skip the body if it still matches. On a match, GET and HEAD get304 Not Modifiedwith no body, and other methods get412 Precondition Failed. On writes,If-None-Match: *means “create this only if nothing exists here yet”.
What is If-None-Match?
A client sends a saved ETag in this header to check whether its stored copy is still current, without downloading the body again. It’s not only a browser cache feature, though. It also works as a precondition on writes. RFC 9110 §13.1.2 defines both uses.
How If-None-Match Works
Messages on this page are examples, not captured traffic. A conditional request looks like this:
GET /notes/current HTTP/1.1
Host: example.com
If-None-Match: "revision-a"
If the current representation still has that tag, the server answers:
HTTP/1.1 304 Not Modified
Date: Mon, 05 Oct 2026 10:00:00 GMT
ETag: "revision-a"
Cache-Control: max-age=60
A 304 has no body, so the client reuses the one it stored earlier. If the tag doesn’t match, the condition passes and the server handles the request as usual. Usually that means a fresh 200 with the new body, but any normal outcome is possible, errors included.
ETag Formats
All of these are valid:
If-None-Match: "revision-a"
If-None-Match: W/"revision-a"
If-None-Match: "revision-a", "revision-b"
If-None-Match: *
The comparison is weak, even when the tags look strong, so "revision-a" and W/"revision-a" count as a match. With a list, any matching member fails the condition. * fails it whenever a current representation exists, whether or not that representation has an ETag, and it has to appear on its own.
Real-World Examples
To create a resource without overwriting one that’s already there, on an API that supports PUT:
curl -i -X PUT https://example.com/notes/new \
-H 'If-None-Match: *' \
-H 'Content-Type: text/plain' --data-binary 'hello'
If something already exists at that URL, you get 412. To update a version you’ve already read, use If-Match instead. Putting the old tag in If-None-Match gets the logic backwards: the write would go through precisely when the current version is different from the one you read.
Server Response Patterns
On a match, GET and HEAD get 304 and every other method gets 412. A 304 has to include Content-Location, Date, ETag and Vary whenever the matching 200 would have carried them. RFC 9110 §15.4.5 has the metadata rules, including what to do with cache fields.
Client Implementation
In the browser, let the HTTP cache handle this for you unless you’re intentionally keeping your own copy of the body and its validator. During normal revalidation, Fetch takes the 304 from the network, refreshes the stored response, and hands that stored response to your JavaScript. So DevTools shows a 304 while your code gets the stored response, usually with response.status === 200. See the Fetch cache algorithm.
ETag isn’t a CORS-safelisted response header, so cross-origin JavaScript can only read it if the server sends Access-Control-Expose-Headers: ETag.
To make the browser revalidate before reusing a cached response, without keeping a second copy yourself:
const response = await fetch('/public-config', { cache: 'no-cache' });
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const config = await response.json();
The Fetch request cache modes treat these two differently: no-cache still stores responses and revalidates the cached one, while no-store skips the HTTP cache entirely. Neither one controls a CDN, which is a separate cache layer. To see what the CDN did, check the response headers and the CDN’s cache logs.
Server Implementation
Parse entity tags properly and compare them weakly. Plain string equality gets it wrong: it treats a strong tag and a weak tag with the same opaque value as different. Splitting the header on every comma breaks too, because a quoted tag can contain commas.
Check If-None-Match before you serve content or perform a write. If a request carries both If-None-Match and If-Modified-Since, ignore If-Modified-Since. And ordinary request errors come first: under RFC 9110 §13.2.1, the server evaluates preconditions only after it would otherwise have succeeded.
Express’s res.send() handles conditional responses for non-streaming bodies. Give it an ETag computed from the exact bytes you’re sending, and it decides whether to send the body or a 304:
import { createHash } from 'node:crypto';
app.get('/public-config', (req, res) => {
const body = JSON.stringify({ theme: 'light' });
const tag = createHash('sha256').update(body).digest('hex');
res.set({
ETag: `"${tag}"`,
'Cache-Control': 'public, max-age=60',
});
res.type('application/json').send(body);
});
If you set status 304 by hand, send it without a body, and keep the metadata the 200 would have had. This example uses a fixed public body so authentication and content negotiation stay out of the picture. Once you add variants or compression, each representation needs its own validator.
Performance Benefits
A 304 saves sending the body. The server may still run the database query, render the page or serialize the data, because it has to work out the current validator somehow. Measure where your time actually goes.
Testing ETags
curl can save the server’s real tag and send it back:
curl -sS --etag-save /tmp/notes.etag \
-D - -o /tmp/notes.body https://example.com/notes/current
curl -sS --etag-compare /tmp/notes.etag \
-D - -o /tmp/notes.next https://example.com/notes/current
The second command sends If-None-Match. Make sure the first response actually included an ETag. A 304 comes back without a body, so keep the original file around.
Weak vs Strong ETags and When to Use Each
A strong validator changes whenever the representation’s bytes change; it isn’t a hash of every HTTP header. A weak validator stays the same across changes the origin considers insignificant. If-None-Match accepts both. If-Match, and ETags used in If-Range, need strong comparison. A plain Range request doesn’t need an ETag at all.
Related Headers
Frequently asked questions
What is If-None-Match?
If-None-Match makes a request conditional on no supplied tag matching. A match returns 304 for GET or HEAD and 412 for other methods.
How does If-None-Match work?
The recipient uses weak ETag comparison. If any supplied tag matches, the condition fails; otherwise the method is processed normally.
Can If-None-Match contain multiple ETags?
Yes, it accepts a comma-separated list of ETags. Any matching member fails the condition. A standalone * fails when a current representation exists.
What is the difference between If-None-Match and If-Match?
If-None-Match succeeds on no match and uses weak comparison; If-Match succeeds on a match and uses strong comparison. Use If-Match to protect an existing version during updates.
Sources
Related
If-Match Header
Learn how the If-Match header makes requests conditional based on ETag matching. Prevent conflicts and lost updates in concurrent editing scenarios.
If-Modified-Since Header
Learn how the If-Modified-Since header requests resources only if modified since a specific date. Reduce bandwidth with efficient conditional caching.
If-Unmodified-Since Header
Learn how the If-Unmodified-Since header makes requests conditional on resources not being modified. Prevent conflicts in concurrent update scenarios.
If-Range Header
Learn how the If-Range header requests partial content only if the resource is unchanged. Efficiently resume downloads without re-fetching entire files.