# 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.

Source: https://howhttpworks.com/headers/if-none-match
Last reviewed: 2026-10-05

> **TL;DR:** `If-None-Match` sends back the ETag you already have and asks the server to skip the body if it still matches. On a match, GET and HEAD get `304 Not Modified` with no body, and other methods get `412 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](https://howhttpworks.com/headers/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](https://www.rfc-editor.org/rfc/rfc9110#section-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:

```http
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
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:

```http
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:

```bash
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](https://howhttpworks.com/headers/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](https://www.rfc-editor.org/rfc/rfc9110#section-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](https://fetch.spec.whatwg.org/#http-network-or-cache-fetch).

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:

```javascript
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](https://fetch.spec.whatwg.org/#concept-request-cache-mode) 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()`](https://expressjs.com/en/5x/api/response/#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:

```javascript
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:

```bash
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

- [ETag](https://howhttpworks.com/headers/etag)
- [If-Match](https://howhttpworks.com/headers/if-match)
- [If-Modified-Since](https://howhttpworks.com/headers/if-modified-since)
- [Cache-Control](https://howhttpworks.com/headers/cache-control)
