ゆるテックノート

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.

Specifications

RFC 9110 §13 / RFC 9110 §15.4.5 / RFC 6585 §3