Khi dữ liệu chưa thay đổi, server không cần gửi lại toàn bộ JSON. Với FastAPI, bạn chỉ cần gắn ETag cho response và kiểm tra header If-None-Match: khớp thì trả 304 Not Modified, không khớp thì trả dữ liệu mới kèm ETag mới. Cách này phù hợp nhất cho các endpoint đọc dữ liệu thay đổi chậm như danh mục hay cấu hình.
Vấn đề
Hãy xem endpoint quen thuộc dưới đây. Mỗi lần client gọi, server đều trả toàn bộ JSON, dù dữ liệu không đổi một byte nào:
from fastapi import FastAPIapp = FastAPI()ITEMS = {"id": 1, "name": "Áo thun", "price": 199000}@app.get("/items/1")def get_item(): return ITEMS
Với một client thì không sao, nhưng với hàng nghìn client polling mỗi phút, băng thông và CPU serialize JSON bị đốt vô ích. Client mobile còn tốn pin và dữ liệu di động chỉ để nhận về thứ mình đã có.
Giải pháp
ETag là một chuỗi định danh cho một phiên bản dữ liệu, thường là hash của nội dung. Server gửi ETag trong response; lần sau client gửi lại giá trị đó qua header If-None-Match. Nếu khớp, server trả 304 và không gửi body:
import hashlibimport jsonfrom fastapi import FastAPI, Header, Responsefrom fastapi.responses import JSONResponseapp = FastAPI()ITEMS = {"id": 1, "name": "Áo thun", "price": 199000}def make_etag(payload: dict) -> str: body = json.dumps(payload, sort_keys=True, ensure_ascii=False) return f'"{hashlib.md5(body.encode("utf-8")).hexdigest()}"'@app.get("/items/1")def get_item(if_none_match: str | None = Header(default=None)): etag = make_etag(ITEMS) if if_none_match == etag: return Response(status_code=304, headers={"ETag": etag}) return JSONResponse(content=ITEMS, headers={"ETag": etag})
Điểm hay nằm ở ba chi tiết. Thứ nhất, sort_keys=True giúp cùng một dữ liệu luôn cho cùng một hash, bất kể thứ tự key trong dict. Thứ hai, so sánh ETag diễn ra trước khi serialize response, nên request trúng cache gần như không tốn tài nguyên. Thứ ba, status 304 vẫn kèm ETag để client làm mới bản cache mà không cần tải lại. Lưu ý: header If-None-Match có thể chứa nhiều ETag hoặc ký tự *, nên trong production hãy xử lý thêm các trường hợp đó.
Áp dụng vào dự án của bạn
Bắt đầu từ một endpoint đọc nhiều và thay đổi ít, ví dụ danh sách tỉnh thành, bảng giá hay bài viết đã xuất bản. Gắn ETag, đo lưu lượng trước và sau một tuần, rồi mới nhân rộng sang các endpoint khác. Khi dữ liệu đổi theo từng bản ghi, hãy đưa updated_at hoặc version của bản ghi vào chuỗi tính hash để ETag đổi đúng lúc dữ liệu đổi.
Bài viết liên quan
- FastAPI: giảm băng thông API bằng GZipMiddleware trong 5 phút
- FastAPI: trả đúng status code và header trong response — 201 Created, 204 No Content và Location trong 5 phút
- FastAPI: đặt cookie đăng nhập an toàn bằng Response.set_cookie trong 5 phút
Câu hỏi thường gặp
ETag mạnh và ETag yếu khác nhau thế nào?
ETag mạnh, ví dụ “abc123”, khẳng định body giống nhau từng byte. ETag yếu, bắt đầu bằng W/, chỉ khẳng định nội dung tương đương về mặt ngữ nghĩa. Với JSON API thông thường, ETag mạnh tính từ hash nội dung là đủ và dễ kiểm chứng nhất.
Có dùng chung ETag với GZipMiddleware được không?
Được. Hai cơ chế nằm ở hai tầng khác nhau: GZip giảm kích thước body khi buộc phải trả 200, còn ETag giúp bỏ hẳn body khi trả 304. Khi kết hợp, hãy kiểm tra header thực tế vì một số proxy có thể thay đổi ETag sau khi nén.
Khi nào không nên dùng ETag?
Không nên dùng cho dữ liệu thay đổi liên tục theo thời gian thực, như bảng giá chứng khoán hay vị trí xe đang chạy, vì client gần như không bao giờ trúng cache. Cũng không dùng ETag cho POST, PUT hay DELETE, vì các phương thức này thay đổi trạng thái chứ không phải đọc dữ liệu.
Client có tự gửi If-None-Match không?
Trình duyệt tự làm việc này với các response GET có thể cache: nó lưu ETag và gửi lại ở lần gọi sau. Với app mobile hoặc script tự viết, bạn cần tự lưu ETag nhận được và gắn vào header If-None-Match trong request tiếp theo.
💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.
