Home / Blog / URL HTTP & cURL

API & webhooks

HTTP Headers in REST APIs: Conditional Reads with ETags

Use ETag and If-None-Match for conditional API reads, distinguish a 304 response from an empty JSON body, and keep cached values tied to the right request.

Use headers to ask whether a representation changed

HTTP headers can carry conditions as well as describe content. A conditional GET lets a client ask whether its stored representation is still current. This is useful when an API repeatedly returns the same resource and supports a validator such as ETag. It is not a way to invent caching support for an endpoint that does not offer it.

Start with one authorized GET request. Record the response body and its ETag together. On a subsequent request for the same representation, send the validator in If-None-Match. A matching response can use status 304 to tell the client to reuse its existing representation.

Handle the no-body branch before parsing JSON

async function conditionalRead(url, previous, fetcher = fetch) {
  const headers = new Headers({Accept: "application/json"});
  if (previous !== undefined) {
    if (previous.url !== url || !previous.etag ||
        !Object.hasOwn(previous, "value")) {
      throw new Error("A matching cached value and ETag are required.");
    }
    headers.set("If-None-Match", previous.etag);
  }
  const response = await fetcher(url, {headers});
  if (response.status === 304) {
    if (previous === undefined) throw new Error("No value to reuse.");
    return {...previous,
      etag: response.headers.get("etag") || previous.etag};
  }
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const value = await response.json();
  return {url, value, etag: response.headers.get("etag")};
}

The example returns a small cache record instead of a bare value. Its 304 branch runs before response.ok because 304 is outside the successful 200–299 range. It does not call json on that branch. A caller may still choose not to store the result when the endpoint's policy disallows storage.

For a first response containing {"title":"Map notes"} with ETag "note-v1", retain both. If the next read is 304, the title remains Map notes. If the next read is 200 with a different title and validator, replace both fields. Never attach the new validator to an old body after a parsing failure.

Assign the returned record only after the promise succeeds. Clearing the existing record before the request completes can destroy a usable representation when a network failure occurs. Whether to show that older representation during an error is a separate interface decision: label it as previously fetched and do not present it as freshly validated by the failed request.

Keep the validator attached to its representation

An ETag is an opaque value. Preserve its quotes and any weak-validator prefix instead of extracting digits or treating it as a modification timestamp. The If-None-Match reference explains the conditional comparison and response behavior.

The helper checks the URL, but a production cache needs a key that also accounts for applicable request variation. Two users, languages or negotiated formats can receive different representations from one URL. Do not share a private cached body merely because its path matches another request.

Respect cache policy and browser behavior

This is an application-level teaching example, not a complete HTTP cache. It does not implement freshness lifetimes, Vary, storage permissions, eviction or concurrent request ownership. Review the service's Cache-Control policy before persisting a body.

A browser can perform cache revalidation itself, so the response observed by application code may differ from a wire-level exchange in the network panel. Do not infer failure merely because JavaScript receives a usable 200 response while the panel records revalidation traffic. Test the actual layer your application controls.

For a cross-origin request, reading ETag and sending If-None-Match also depend on the server's CORS configuration. The response validator must be exposed when required, and the request header must be permitted. A front-end script cannot grant itself those permissions.

Test replacement, reuse and rejection separately

Use synthetic responses for a first 200, a matching 304 and a changed 200. Assert that the second request contains the exact original validator and that a changed body replaces the stored value. Include a JSON null body to prove that a valid falsy value is not confused with a missing cache entry.

Reject a cache record for a different URL before making a request. Also test 304 without a prior value, a server error and malformed JSON. The REST API Testing Tool can display a readable response and exposed headers, but it does not provide this helper's application cache or certify an endpoint's caching policy.