HTTP Caching with Last-Modified
and If-Modified-Since
Headers
by Christoph Schiessl on Python and FastAPI
In one of my previous articles, I explained the basics of ETag
-based caching. Today, I want to introduce you to timestamp-based caching as an alternative, which relies on the Last-Modified
response header and the If-Modified-Since
request header. Therefore, similar to HTTP caching controlled by the ETag
and If-None-Match
headers, both client and server-side support are required.
Last-Modified
response header
Imagine having a dist
directory with a single index.html
file and a simple Python application that uses FastAPI's StaticFiles
feature to serve this directory.
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/", StaticFiles(directory="dist", html=True), name="dist")
uvicorn.run(app=app, port=3000)
$ tree --noreport .
.
├── app.py
└── dist
└── index.html
If you start your FastAPI app with python app.py
and request the index.html
file, you'll see the Last-Modified
header in the HTTP response:
$ http GET http://localhost:3000/index.html
HTTP/1.1 200 OK
content-length: 62
content-type: text/html; charset=utf-8
date: Sun, 21 Apr 2024 16:22:10 GMT
etag: "d5eabc651bf2490653431623615b1546"
last-modified: Sun, 21 Apr 2024 14:15:02 GMT
server: uvicorn
<!doctype html><meta charset=utf-8><title>/index.html</title>
In the case of FastAPI's StaticFiles
, the timestamp comes from the modification timestamp of the file on disk. So, if you touch index.html
or modify the file in some other way, the modification timestamp and the value of the header change. However, the timestamp could also come from a different source, such as a database. Please note the timestamp's format: It must be formatted strictly according to the time format defined in RFC 5322. If you ever have to work with RFC 5332-formatted dates, consider using the email.utils
module, which is the library that FastAPI uses internally.
If-Modified-Since
request header
The opposite side of the Last-Modified
header is the If-Modified-Since
request header. This header is set by the HTTP client (e.g., your browser) and must adhere to the same formatting. If the request header is present and the HTTP server supports timestamp-based caching, then the following process is followed for incoming requests:
- Parse the
If-Modified-Since
header into adatetime
object that supports comparisons with otherdatetime
objects. - For the requested resource (e.g.,
index.html
), determine thedatetime
for theLast-Modified
response header. - If
Last-Modified
is less than or equal toIf-Modified-Since
, the server responds with status304 Not Modified
. - Otherwise, the server will handle the request in the normal way and respond with status
200 OK
.
The whole point of all of this is to save bandwidth by not transferring payloads that the client has already cached. The trick is that 304 Not Modified
responses never contain a body, but 200 OK
responses do.
$ # 200 OK if 'Last-Modified' > 'If-Modified-Since' ...
$ http GET http://localhost:3000/index.html 'If-Modified-Since: "Sun, 21 Apr 2024 14:15:01 GMT"'
HTTP/1.1 200 OK
content-length: 62
content-type: text/html; charset=utf-8
date: Sun, 21 Apr 2024 16:24:23 GMT
etag: "d5eabc651bf2490653431623615b1546"
last-modified: Sun, 21 Apr 2024 14:15:02 GMT
server: uvicorn
<!doctype html><meta charset=utf-8><title>/index.html</title>
$ # 304 Not Modified if 'Last-Modified' == 'If-Modified-Since' ...
$ http GET http://localhost:3000/index.html 'If-Modified-Since: "Sun, 21 Apr 2024 14:15:02 GMT"'
HTTP/1.1 304 Not Modified
date: Sun, 21 Apr 2024 16:24:08 GMT
etag: "d5eabc651bf2490653431623615b1546"
server: uvicorn
$ # 304 Not Modified if 'Last-Modified' < 'If-Modified-Since' ...
$ http GET http://localhost:3000/index.html 'If-Modified-Since: "Sun, 21 Apr 2024 14:15:03 GMT"'
HTTP/1.1 304 Not Modified
date: Sun, 21 Apr 2024 16:24:08 GMT
etag: "d5eabc651bf2490653431623615b1546"
server: uvicorn
This is everything for today. Thank you for reading, and see you next time!