Huny's Dev Blog
huny.dev

How to Implement HTTP 304 Responses with ETag and Conditional GET

Use HTTP ETag and If-None-Match to skip unchanged response bodies. Reproduce 200 and 304 responses in Python and see how no-cache and compression affect the design.

Hun Jang
Hun Jang Nov 8, 2026

If an HTTP GET keeps transferring the same data, add an ETag to the server response and check If-None-Match on the next request. When the stored response matches the current one, the server returns 304 Not Modified without a body. This article reproduces the behavior for a public GET endpoint on a local Python server and verifies that a body change brings back 200 OK.

Illustration of a response fingerprint being checked between a server and a cache

Why the same URL keeps returning 200

Even if a cache holds the response body, it needs a validator to compare against the current version before it can skip the transfer this way. An ETag identifies the representation the server selected for the request. The client stores the tag and sends it in If-None-Match when it checks the resource again. If the current representation’s tag matches on a GET, the server returns 304. The cache can reuse the stored body and update relevant metadata from the response. RFC 9110 §13.1.2 · RFC 9110 §15.4.5

With Cache-Control: no-cache, a response can be stored, and the cache must validate it with the origin before reuse. Use no-store to prohibit storage. Revalidation still takes a network round trip even when it skips the body transfer. RFC 9111 §5.2.2.4–5

Build the ETag from the response bytes

The example takes one snapshot of the response bytes, computes SHA-256 from those bytes, and wraps the digest in double quotes to form a strong ETag. This avoids hashing one version and sending a different one if the data changes in between. A tag based solely on a value such as a deployment version can miss a representation change and cause a client to reuse an outdated copy.

Save the code as http_etag.py and run python3 http_etag.py. It uses only the standard library and binds the server to a loopback address on a port assigned by the operating system.

Verify 200 and 304 before and after a change

This output comes from a run on Linux with Python 3.12.14 on November 8, 2026. The code checks the status, ETag, and body for all four requests, along with the Cache-Control value on each response.

The second request sends the first response’s tag. The body is unchanged, so the server returns an empty 304. The third request also sends the first tag, but the body has changed, so the server returns 200 with a new tag and the version 2 body. The last request sends the new tag and receives an empty 304 again.

Compression and production considerations

The example uses string equality because the client echoes one strong tag issued by the server. In production, If-None-Match handling must also support weak comparison, multiple tags, and the * wildcard. Keep validator generation separate from conditional request parsing and check that your framework’s conditional request handling implements these rules. RFC 9110 §13.1.2

If you select between gzip and uncompressed representations based on Accept-Encoding, send Vary: Accept-Encoding so caches distinguish the variants, and give the two responses distinct strong ETags when their representation data differs. If a proxy or CDN applies compression, inspect the headers the client ultimately receives. The reproduction verified the HTTP responses exchanged by a Python client and a local server. Browser caches and CDNs were outside the test scope. RFC 9110 §8.8.3.3 · RFC 9111 §4.1

한국어 버전 · HTTP ETag와 조건부 GET으로 304 응답 구현하기HTTP ETag와 조건부 GET으로 304 응답 구현하기

추천 글