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 ETag
s. So what's the difference?
Strong vs. Weak ETag
Headers
The whole point of ETag
s 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 ETag
s. This can help you avoid transferring the resource and thereby reduce bandwidth requirements. The difference between strong and weak ETag
s is how they are calculated.
Strong ETag
s are derived from a resource in a byte-by-byte fashion. In other words, if the strong ETag
s of two resources are the same, you can assume that the two resources are literally identical down to the last byte.
In contrast, weak ETag
s 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:
PATCH /enable-minification
— turns on minification by assigning the global variablecurrent_json
tominified_json
, thereby changing the return value of subsequent requests.PATCH /disable-minification
— turns off minification by assigning the global variablecurrent_json
tooriginal_json
, thereby changing the return value of subsequent requests.GET /with-strong-etag
— returnscurrent_json
with strongETag
response header (i.e., MD5 sum ofcurrent_json
).GET /with-weak-etag
— returnscurrent_json
with weakETag
response header (i.e., MD5 sum oforiginal_json
).
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 ETag
s 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 ETag
s 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 ETag
s, 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!