Conditional requests with ETag
Conditional requests help avoid lost updates and save bandwidth. This is a practical RFC 9110-aligned recap of ETag-based flows.
Key headers
Pick the header by intent. ETags come in strong ("abc") and weak (W/"abc") forms.
Conditional headers
| Header | Purpose | Response example |
|---|---|---|
| If-None-Match | Weak tag comparison; * checks for a current representation at the target URI. | On match: GET/HEAD 304; other methods 412 |
| If-Modified-Since | Time-based GET/HEAD revalidation. Ignored when If-None-Match is present. | Not modified since the given time: 304 |
| If-Match | Strong comparison before performing an action; detects conflicting changes. | Failed condition: normally 412 |
| If-Unmodified-Since | Requires no modification after the given time. Ignored when If-Match is present. | Newer modification: normally 412 |
| If-Range | Use with Range on GET. A mismatch means ignoring Range and proceeding with a full response. Weak ETags are not allowed. | GET (Range): 206 or 200 |
Common flows
Think in two buckets: cache saving and conflict avoidance.
Patterns
- Cache revalidation: send the saved ETag in If-None-Match on GET/HEAD. A weak comparison match produces 304, allowing reuse of the stored representation.
- Create without overwriting: PUT to the intended resource URI with If-None-Match: *. A current representation causes 412. The condition applies to the target URI, not to duplicate child entries submitted by POST to a collection.
- Optimistic locking: GET a strong ETag, then send If-Match with PUT/PATCH. After 412, fetch the latest data and review changes before resubmitting. The server must make the condition check and update one indivisible operation.
- Conditional deletes: a failed If-Match normally returns 412 without deleting. Normal request checks, such as authentication failures or 404, can take precedence. Idempotency does not require returning 204 every time.
- Partial resume: Range + If-Range to continue downloads; mismatch falls back to full 200.
Status codes
Normal request checks, including authentication and authorization, come before condition evaluation. A matching tag is not the same as a true condition for every header.
Code mapping
| Situation | Return | Note |
|---|---|---|
| GET/HEAD: matching tag or no modification since the given time | 304 Not Modified | No body. See header requirements below; Last-Modified is not always mandatory. |
| Mismatch (If-Match / If-Unmodified-Since on writes) | 412 Precondition Failed | Tell the client to re-read. |
| Precondition header required but missing | 428 Precondition Required | RFC 6585 §3: explain the required condition. A 428 response must not be stored by a cache. |
Design notes
Define how you mint ETags so the team can reason about cache and locking.
Tips
- A strong ETag must change when representation data changes. If using version numbers, avoid assigning the same strong tag to different language or encoded representations.
- If-None-Match uses weak comparison; If-Match uses strong comparison. W/"abc" does not match "abc" for If-Match. Use a date in If-Range only when no ETag is available and the date is a strong validator.
- Evaluate If-Match (otherwise If-Unmodified-Since), then If-None-Match (otherwise If-Modified-Since for GET/HEAD), then If-Range for GET. Passing If-Match does not skip If-None-Match (RFC 9110 §13.2.2).
- For 304, send Content-Location, Date, ETag, Vary, Cache-Control, and Expires if they would have been sent in the corresponding 200 response. Do not send content or trailers (RFC 9110 §15.4.5).
- A failed If-Match or If-Unmodified-Since can still receive 2xx when the requested state change can be determined to have already succeeded. This exception is not a way to hide arbitrary failures.
Takeaway
304 enables reuse; 412 reports a failed condition. Continue with storage and revalidation and status code choices.