Showcasing Weak and Strong ETag Headers with FastAPI

by Christoph Schiessl on Python and FastAPI

I recently wrote about HTTP caching with strong ETag headers, but sometimes it makes more sense to use weak ETags. So what's the difference?

Strong vs. Weak ETag Headers

The whole point of ETags is to have a proxy to determine the equality of two resources in HTTP traffic. Instead of directly comparing two potentially very big resources, you can compare their much smaller ETags. This can help you avoid transferring the resource and thereby reduce bandwidth requirements. The difference between strong and weak ETags is how they are calculated.

Strong ETags are derived from a resource in a byte-by-byte fashion. In other words, if the strong ETags of two resources are the same, you can assume that the two resources are literally identical down to the last byte.

In contrast, weak ETags are more liberal because they require only semantic equality, and this is best explained with a simple example.

original_json = b"""{\n  "a": 1,\n  "b": 2\n}"""
minified_json = b"""{"a":1,"b":2}"""

While the two JSON strings above are not byte-by-byte identical, they are semantically equivalent, which means that if you parse them, you get precisely the same data structure for both inputs (i.e., a dictionary with two keys, a and b). So, the strong ETag of these two strings would tell you that they are not identical, but their weak ETag could tell you that they are semantically equivalent.

I'm saying could because there are no fixed rules for this. It's up to you to decide which resource characteristics are insignificant for clients and can be ignored for your use case. I only picked JSON and the removal of insignificant whitespace as an example because they are easy to understand.

Test-Drive with FastAPI

A demonstration says more than 1000 words, so here is a small FastAPI application that is based on the JSON example above and implements four endpoints:

Furthermore, both GET endpoints can also respond with the status 304 Not Modified and without content if the incoming request includes an If-None-Match request header, whose value is identical to the computed value for the ETag response header. This last behavior makes the resources cacheable for clients supporting the ETag/If-None-Match headers.

from fastapi import FastAPI, Request, Response, status
from hashlib import md5
import uvicorn

app = FastAPI()

original_json = b"""{\n  "a": 1,\n  "b": 2\n}"""
minified_json = b"""{"a":1,"b":2}"""
current_json = original_json

@app.patch("/disable-minification", status_code=status.HTTP_204_NO_CONTENT)
def disable_minification() -> None:
  global current_json # needed for the assignment of the global variable
  current_json = original_json

@app.patch("/enable-minification", status_code=status.HTTP_204_NO_CONTENT)
def enable_minification() -> None:
    global current_json
    current_json = minified_json

@app.get("/with-strong-etag")
def with_strong_etag(request: Request) -> Response:
    etag = f'"{md5(current_json).hexdigest()}"'
    return _with_etag(etag, request)

@app.get("/with-weak-etag")
def with_weak_etag(request: Request) -> Response:
    etag = f'\\W"{md5(original_json).hexdigest()}"'
    return _with_etag(etag, request)

def _with_etag(etag: str, request: Request) -> Response:
    common = {"media_type": "application/json", "headers": {"ETag": etag}}
    if request.headers.get("If-None-Match") == etag:
        return Response(status_code=status.HTTP_304_NOT_MODIFIED, **common)
    return Response(status_code=status.HTTP_200_OK, content=current_json, **common)

if __name__ == "__main__":
    uvicorn.run(app=app)

Anyway, you can run this application with python app.py and then use curl to trigger the endpoints. As you can see, if you turn on/off minification, the strong ETag changes, but the weak ETag stays the same.

$ curl -X PATCH localhost:8000/disable-minification # original_json
$ curl -i -s -X GET localhost:8000/with-strong-etag | grep etag
etag: "b4e6790011336adc9d1e96d14780e0ce"
$ curl -i -s -X GET localhost:8000/with-weak-etag | grep etag
etag: \W"b4e6790011336adc9d1e96d14780e0ce"

$ curl -X PATCH localhost:8000/enable-minification # minified_json
$ curl -i -s -X GET localhost:8000/with-strong-etag | grep etag
etag: "608de49a4600dbb5b173492759792e4a"
$ curl -i -s -X GET localhost:8000/with-weak-etag | grep etag
etag: \W"b4e6790011336adc9d1e96d14780e0ce"

This is precisely what I explained before. Weak ETags guarantee semantic equality, and minification doesn't affect the meaning of the JSON. Hence, the weak ETag value doesn't change when the minification is turned on or off. This statement is not true for strong ETags because these guarantee byte-by-byte identicality. Obviously, original_json and minified_json are not identical, and therefore, the strong ETag value changes when minification is turned on or off.

Lastly, we can also demonstrate how the caching behavior differs for strong and weak ETags, but this really shouldn't surprise you after reading this article ...

$ curl -X PATCH localhost:8000/disable-minification # original_json
$ curl -i -s -X GET localhost:8000/with-strong-etag | grep etag
etag: "b4e6790011336adc9d1e96d14780e0ce"
$ curl -i -s -X GET localhost:8000/with-strong-etag \
  --header 'If-None-Match: "b4e6790011336adc9d1e96d14780e0ce"' | head --lines 1
HTTP/1.1 304 Not Modified
$ curl -i -s -X GET localhost:8000/with-weak-etag | grep etag
etag: \W"b4e6790011336adc9d1e96d14780e0ce"
$ curl -i -s -X GET localhost:8000/with-weak-etag \
  --header 'If-None-Match: \W"b4e6790011336adc9d1e96d14780e0ce"' | head --lines 1
HTTP/1.1 304 Not Modified

$ curl -X PATCH localhost:8000/enable-minification # minified_json
$ curl -i -s -X GET localhost:8000/with-strong-etag \
  --header 'If-None-Match: "b4e6790011336adc9d1e96d14780e0ce"' | head --lines 1
HTTP/1.1 200 OK
$ curl -i -s -X GET localhost:8000/with-weak-etag \
  --header 'If-None-Match: \W"b4e6790011336adc9d1e96d14780e0ce"' | head --lines 1
HTTP/1.1 304 Not Modified

That's everything for today. Thank you very much for reading, and see you soon!

Ready to Learn More Web Development?

Join my Mailing List to receive two articles per week.


I send two weekly emails on building performant and resilient Web Applications with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.

Continue Reading?

Here are a few more Articles for you ...


HTTP Caching with Last-Modified and If-Modified-Since Headers

Learn about timestamp-based caching in HTTP using the Last-Modified and If-Modified-Since headers, with Python's FastAPI as an example.

By Christoph Schiessl on Python and FastAPI

How to Find the First/Last Day of a Year from a given Date

Learn how to find the first and last day of a year with Python's datetime module. This article explains step by step what you need to know.

By Christoph Schiessl on Python

Using the If-Match Header to Avoid Collisions

Prevent lost updates in HTTP APIs using ETag and If-Match headers to block conflicting updates and ensure data integrity.

By Christoph Schiessl on Python and FastAPI

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast web applications.


I'm available for hire as a freelance web developer, so you can take advantage of the more than a decade of experience I have collected working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm also a skilled writer who takes pride in breaking down technical concepts into the simplest possible terms.