Đánh giá các tiêu đề bộ nhớ đệm HTTP (Cache-Control, Expires, ETag, Last-Modified, Vary, Set-Cookie) cho các trang web, tài sản tĩnh (assets) và API. Tính toán thời gian còn hiệu lực (freshness) trên trình duyệt và CDN, gắn cờ cảnh báo các phản hồi chứa cookie hoặc dữ liệu cá nhân mà CDN có nguy cơ lưu chung, trang HTML được lưu đệm quá lâu, tài sản đã gắn mã băm (hashed) lại lưu đệm quá ngắn, thuộc tính immutable gắn trên các URL có nội dung thay đổi, phân mảnh bộ nhớ đệm do Vary, thiếu bộ xác thực (validator), lưu đệm theo suy đoán (heuristic caching) và lưu đệm cả các phản hồi lỗi. Đề xuất tiêu đề phù hợp cho từng loại tài nguyên. Bao gồm công cụ kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ cờ `--self-test`.
---
name: http-cache-header-reviewer
description: Đánh giá các tiêu đề bộ nhớ đệm HTTP (Cache-Control, Expires, ETag, Last-Modified, Vary, Set-Cookie) cho các trang web, tài sản tĩnh (assets) và API. Tính toán thời gian còn hiệu lực (freshness) trên trình duyệt và CDN, gắn cờ cảnh báo các phản hồi chứa cookie hoặc dữ liệu cá nhân mà CDN có nguy cơ lưu chung, trang HTML được lưu đệm quá lâu, tài sản đã gắn mã băm (hashed) lại lưu đệm quá ngắn, thuộc tính immutable gắn trên các URL có nội dung thay đổi, phân mảnh bộ nhớ đệm do Vary, thiếu bộ xác thực (validator), lưu đệm theo suy đoán (heuristic caching) và lưu đệm cả các phản hồi lỗi. Đề xuất tiêu đề phù hợp cho từng loại tài nguyên. Bao gồm công cụ kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---
# Trình Đánh Giá Tiêu Đề Bộ Nhớ Đệm HTTP (HTTP Cache Header Reviewer)
Bạn giúp các lập trình viên web cấu hình bộ nhớ đệm (caching) chuẩn xác: tăng tốc độ cho các lần truy cập lặp lại và giảm hóa đơn chi phí CDN mà không làm trang web bị cũ sau khi triển khai (deploy) hoặc tệ hơn là trả nhầm dữ liệu của người dùng này cho người dùng khác. Bạn thu thập các tiêu đề phản hồi (response headers) thực tế, giải thích cách từng tầng bộ nhớ đệm sẽ thực sự xử lý chúng, và đề xuất tiêu đề chuẩn cho từng loại tài nguyên kèm theo danh sách khắc phục ngắn gọn.
## Các tệp trong kỹ năng này
- `scripts/cache_audit.py` - đọc các khối phản hồi (đầu ra từ `curl -sI` hoặc DevTools) và in ra loại tài nguyên, thời gian sống (TTL) trên trình duyệt và CDN, cơ chế xác thực lại (revalidation), các cờ cảnh báo và tiêu đề `Cache-Control` được đề xuất (chỉ dùng thư viện chuẩn của Python 3)
- `references/cache-control-directives.md` - giải thích mọi chỉ thị (directive), quy tắc tính thời gian hiệu lực (freshness) và lưu đệm suy đoán (heuristic), bộ xác thực (validators), tiêu đề `Vary`, các mã trạng thái có thể lưu đệm
- `references/caching-strategy-playbook.md` - các tiêu đề khuyến nghị theo từng loại tài nguyên, đảm bảo an toàn khi triển khai (deploy), các lưu ý về CDN, cách thu thập và kiểm chứng tiêu đề
- `templates/cache-review-report.md` - mẫu báo cáo đối chiếu tiêu đề hiện tại với tiêu đề khuyến nghị cùng danh sách các mục cần sửa
- `examples/shop-responses.txt` - tám phản hồi từ một trang thương mại điện tử mẫu chứa các lỗi cấu hình phổ biến
## Quy trình thực hiện
### 1. Thu thập các tiêu đề
Yêu cầu danh sách các URL đại diện: trang chủ, một trang sau khi đăng nhập, một tệp JS/CSS có gắn mã băm (hashed), một tệp kịch bản không gắn mã băm, một hình ảnh, một phông chữ, một lệnh gọi API công khai, một lệnh gọi API cá nhân, và một phản hồi báo lỗi. Thu thập bằng lệnh `curl -sI <url>` (thêm `-H 'Cookie: ...'` hoặc `-H 'Authorization: ...'` đối với các yêu cầu đã đăng nhập, tuyệt đối không dán token thật vào báo cáo) hoặc dùng DevTools. Đặt từng phản hồi theo định dạng khối của tệp `examples/shop-responses.txt`: một dòng `### METHOD URL`, các dòng tùy chọn `> Request-Header: value`, dòng trạng thái (status line), rồi đến các tiêu đề phản hồi.
### 2. Chạy công cụ kiểm tra
```
python3 scripts/cache_audit.py examples/shop-responses.txt
python3 scripts/cache_audit.py headers.txt --json > cache-report.json
python3 scripts/cache_audit.py --self-test
```
Mã thoát (exit code) bằng 1 có nghĩa là phát hiện vấn đề mức độ nghiêm trọng cao, bằng 2 có nghĩa là không thể phân tích cú pháp dữ liệu đầu vào.
### 3. Giải thích các phát hiện
Thứ tự ưu tiên: quyền riêng tư là trên hết (AUTH_SHARED_CACHE, PUBLIC_WITH_COOKIE, PERSONAL_NOT_PRIVATE), tiếp theo là tính đúng đắn sau khi triển khai bản mới (HTML_LONG_TTL, IMMUTABLE_UNHASHED, ERROR_CACHED), sau đó đến hiệu năng (HASHED_SHORT_TTL, ASSET_NO_CACHING, VARY_FRAGMENTS, NO_VALIDATOR, HEURISTIC_ONLY) và tính chuẩn mực trong cấu hình (INVALID_DIRECTIVE, NO_STORE_CONFLICT, EXPIRES_ONLY, PRAGMA_ONLY). Sử dụng tệp `references/cache-control-directives.md` để giải thích lý do.
### 4. Đề xuất tiêu đề chuẩn
Sử dụng tệp `references/caching-strategy-playbook.md`. Đưa ra tiêu đề chính xác cho từng loại tài nguyên và chỉ rõ vị trí cần thiết lập trong hệ thống công nghệ (stack) của người dùng (Nginx `add_header`, Express `res.set`, Next.js `headers()`, siêu dữ liệu S3/CloudFront, quy tắc Cloudflare). Nhắc tới nguyên tắc triển khai: lưu đệm tài sản có mã băm trong một năm, còn tệp HTML thì luôn xác thực lại (revalidate).
### 5. Xác minh và lập báo cáo
Điền thông tin vào mẫu `templates/cache-review-report.md`. Sau khi thay đổi cấu hình, hãy thu thập lại các tiêu đề và chạy lại công cụ kiểm tra; đồng thời kiểm tra các tiêu đề HIT/MISS của CDN.
## Ví dụ kết quả
Đối với tệp `examples/shop-responses.txt`: 5 phát hiện mức độ cao (trang HTML trang chủ lưu đệm 1 ngày, tệp `widget.js` đặt `immutable` nhưng không có mã băm, API giỏ hàng có cookie phiên làm việc lại bị CDN lưu đệm trong 2 phút, trang đăng nhập có `Set-Cookie` nhưng bị đánh dấu `public` và lưu đệm 10 phút), 4 phát hiện mức trung bình (`app.3f9a1c7b.js` chỉ lưu đệm 1 giờ, `hero.jpg` lưu đệm theo suy đoán 8,1 ngày, dùng `Vary: Cookie`, lỗi 503 bị lưu đệm trong 1 giờ) và 1 phát hiện mức thấp (API giỏ hàng không có bộ xác thực); chỉ có duy nhất tệp `vendor.81d2e0aa.css` là đã được cấu hình lý tưởng.
## Quy tắc
- Luôn coi bất kỳ nguy cơ nào khiến bộ nhớ đệm dùng chung (shared cache) lưu trữ dữ liệu cá nhân là vấn đề ưu tiên hàng đầu.
- Không yêu cầu hoặc lưu giữ cookie hay token thật; chỉ cần dùng các giá trị giữ chỗ (placeholders) là đủ.
- Khuyến nghị xóa bộ nhớ đệm (purge) hoặc đánh phiên bản URL (URL versioning) khi thay đổi thời gian sống (TTL) của các tài nguyên đang chạy thực tế.
- Nêu rõ khi nào hành vi phụ thuộc vào cấu hình riêng của nhà cung cấp CDN và đề xuất người dùng kiểm tra cấu hình đó.
FILE:references/cache-control-directives.md
# Các Chỉ Thị Cache-Control và Quy Tắc Tính Thời Gian Hiệu Lực
## Các chỉ thị trong phản hồi (Response directives)
| Chỉ thị | Ý nghĩa |
|---|---|
| max-age=N | Còn hiệu lực (fresh) trong N giây ở bất kỳ bộ nhớ đệm nào (cả trình duyệt lẫn bộ nhớ đệm dùng chung) |
| s-maxage=N | Ghi đè giá trị max-age chỉ dành riêng cho các bộ nhớ đệm dùng chung (CDN, proxy) |
| no-cache | Có thể được lưu trữ, nhưng bắt buộc phải xác thực lại (revalidate) với máy chủ gốc trước mỗi lần sử dụng |
| no-store | Tuyệt đối không được lưu trữ ở bất cứ đâu; ghi đè lên mọi chỉ thị khác |
| private | Chỉ trình duyệt của người dùng mới được phép lưu trữ; các CDN và proxy không được phép lưu |
| public | Bộ nhớ đệm dùng chung được phép lưu trữ ngay cả khi thông thường phản hồi đó không được lưu đệm (ví dụ khi có tiêu đề Authorization) |
| must-revalidate | Một khi đã hết hạn (stale), tuyệt đối không được phục vụ nếu chưa xác thực lại thành công với máy chủ gốc |
| proxy-revalidate | Tương tự như must-revalidate, nhưng chỉ áp dụng cho các bộ nhớ đệm dùng chung |
| immutable | Sẽ không bao giờ thay đổi trong suốt thời gian còn hiệu lực; trình duyệt bỏ qua bước xác thực lại khi người dùng tải lại trang |
| stale-while-revalidate=N | Cho phép phục vụ bản cũ đã hết hạn trong N giây trong khi âm thầm làm mới dưới nền |
| stale-if-error=N | Cho phép phục vụ bản cũ đã hết hạn trong N giây nếu máy chủ gốc trả về lỗi 5xx hoặc không thể kết nối |
| no-transform | Các trạm trung gian không được phép sửa đổi phần thân phản hồi (như nén lại hình ảnh, v.v.) |
Những nhầm lẫn phổ biến: `no-cache` KHÔNG có nghĩa là "không lưu vào bộ nhớ đệm" (đó là chức năng của `no-store`); giá trị của `max-age` bắt buộc phải là số nguyên giây (`max-age=10m` là không hợp lệ và sẽ bị bỏ qua); `Pragma: no-cache` chỉ có tác dụng trên chiều yêu cầu gửi đi (request).
## Thời gian sống còn hiệu lực (Freshness lifetime - RFC 9111)
1. Bộ nhớ đệm dùng chung (Shared cache): ưu tiên `s-maxage`, nếu không có thì lấy `max-age`, nếu không có nữa thì lấy `Expires - Date`.
2. Trình duyệt (Browser): ưu tiên `max-age`, nếu không có thì lấy `Expires - Date`.
3. Khi không có cả hai tiêu đề trên nhưng có `Last-Modified`: các bộ nhớ đệm có thể tính theo suy đoán (heuristic), thường lấy bằng 10% của `(Date - Last-Modified)`. Một tệp được sửa đổi lần cuối cách đây 80 ngày có thể được tái sử dụng suốt 8 ngày mà không cần kiểm tra lại.
4. Chỉ thị `private` đặt thời gian sống trên bộ nhớ đệm dùng chung về 0. Chỉ thị `no-store` đặt cả hai về 0.
## Bộ xác thực (Validators) và cơ chế xác thực lại (Revalidation)
- ETag (mạnh `"abc"` hoặc yếu `W/"abc"`) -> phía máy khách gửi kèm tiêu đề `If-None-Match`.
- Last-Modified -> phía máy khách gửi kèm tiêu đề `If-Modified-Since`.
- Nếu khớp, máy chủ trả về mã `304 Not Modified` không kèm phần thân (body), rất nhẹ và tiết kiệm băng thông. Nếu không có bộ xác thực, mỗi lần xác thực lại đều phải tải về toàn bộ phần thân phản hồi.
## Tiêu đề Vary
- `Vary: Accept-Encoding` là bình thường và an toàn.
- `Vary: Cookie`, `User-Agent` hoặc `Referer` sẽ tạo ra một bản ghi bộ nhớ đệm riêng cho mỗi giá trị khác nhau, đồng nghĩa với việc hầu như không bao giờ trúng bộ nhớ đệm (cache hit) trên CDN.
- `Vary: *` có nghĩa là phản hồi này không bao giờ có thể tái sử dụng.
## Các mã trạng thái (Status codes)
Được phép lưu đệm theo mặc định khi còn hiệu lực: 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501. Các phản hồi lỗi (4xx/5xx) chỉ nên lưu đệm rất ngắn hoặc không lưu đệm, nếu không CDN có thể hiển thị trang báo sập hệ thống suốt nhiều giờ liền. Hãy sử dụng `stale-if-error` để phục vụ bản sao hợp lệ gần nhất thay thế.
FILE:references/caching-strategy-playbook.md
# Cẩm Nang Chiến Lược Bộ Nhớ Đệm
## Các tiêu đề khuyến nghị theo từng loại tài nguyên
| Tài nguyên | Cache-Control | Ghi chú |
|---|---|---|
| Trang HTML, khung ứng dụng (app shell) | `no-cache` + ETag | Luôn mới sau mỗi lần triển khai (deploy); phản hồi 304 giúp tiết kiệm băng thông |
| Trang HTML tiếp thị (Marketing) trên CDN | `public, max-age=0, s-maxage=300, stale-while-revalidate=60` | Xóa bộ nhớ đệm CDN (purge) khi xuất bản nội dung mới |
| Tệp JS/CSS có mã băm nội dung (`app.3f9a1c7b.js`) | `public, max-age=31536000, immutable` | Bản dựng (build) mới = URL mới |
| Tệp JS/CSS không có mã băm | `public, max-age=300, stale-while-revalidate=86400` | Hoặc bổ sung cơ chế gắn mã băm vào quy trình build |
| Hình ảnh, tệp người dùng tải lên có URL cố định | `public, max-age=86400, stale-while-revalidate=604800` | Đánh phiên bản cho URL khi hình ảnh thay đổi |
| Phông chữ (URL có đánh phiên bản) | `public, max-age=31536000, immutable` | Đồng thời gửi kèm các tiêu đề CORS |
| Dữ liệu API công khai (giá cả, danh mục sản phẩm) | `public, max-age=30, s-maxage=300, stale-if-error=600` | Giữ TTL ngắn; xóa bộ nhớ đệm khi dữ liệu thay đổi |
| Dữ liệu API cá nhân (giỏ hàng, hồ sơ cá nhân) | `private, no-cache` + ETag | Tuyệt đối không đặt là public |
| Thông tin bí mật, ngân hàng, hồ sơ y tế, token dùng một lần | `no-store` | Áp dụng cả trên trang đăng xuất và trang quản lý tài khoản |
| Chuyển hướng 301 (Redirect 301) | Bắt đầu với `max-age=86400` | Trình duyệt có thể lưu đệm vĩnh viễn một mã 301 không kèm giới hạn thời gian |
| Các lỗi 5xx | `no-store` hoặc `max-age=0`, kết hợp dùng `stale-if-error` trên các phản hồi thành công | |
## Đảm bảo an toàn khi triển khai (Deploy safety)
1. Gắn mã băm (hash) vào tên của mọi tệp tài sản được tạo ra khi build, sau đó lưu đệm chúng trong thời hạn một năm.
2. Cho tệp HTML xác thực lại (revalidate) để nó luôn trỏ tới các tệp có mã băm mới nhất hiện tại.
3. Giữ các tệp tài sản gắn mã băm của bản cũ tiếp tục trực tuyến ít nhất qua một đợt phát hành để các tab trình duyệt đang mở sẵn vẫn hoạt động bình thường.
4. Tuyệt đối không gắn chỉ thị `immutable` lên một URL mà nội dung của nó có thể thay đổi.
## Các lưu ý về CDN
- Hầu hết các CDN chỉ bỏ qua `private`/`no-store` khi bị cấu hình sai; hãy kiểm tra tùy chọn "tôn trọng tiêu đề từ máy chủ gốc" (respect origin headers).
- Nhiều CDN mặc định không lưu đệm các phản hồi có chứa `Set-Cookie`; nhưng một số CDN vẫn lưu nếu có đặt chỉ thị `public`. Hãy loại bỏ `Set-Cookie` khỏi các phản hồi tài nguyên tĩnh.
- Sử dụng tiêu đề `Surrogate-Control` hoặc `CDN-Cache-Control` nếu CDN có hỗ trợ và bạn muốn thiết lập TTL trên CDN tách biệt với TTL trên trình duyệt.
- Chuẩn hóa khóa bộ nhớ đệm (cache key) (bỏ qua các chuỗi truy vấn theo dõi quảng cáo như `utm_*`).
## Cách thu thập các tiêu đề
```
curl -sI https://example.com/ ; curl -sI -H 'Cookie: session=...' https://example.com/api/me
curl -s -o /dev/null -D - https://example.com/assets/app.js # Dùng GET thay vì HEAD
```
Một số máy chủ trả lời phương thức HEAD khác với phương thức GET; khi nghi ngờ, hãy sử dụng cú pháp GET. Trong Chrome DevTools: mở tab Network, chọn một yêu cầu, vào mục Headers, tìm phần Response Headers (sao chép dưới dạng văn bản).
## Kiểm tra kết quả
- Tải lại trang hai lần trong DevTools: các tài sản có gắn mã băm phải hiển thị "(memory cache)" hoặc "(disk cache)", còn trang HTML phải hiển thị mã 304 hoặc 200.
- Đối với CDN: quan sát các tiêu đề `Age`, `X-Cache` hoặc `CF-Cache-Status` (HIT / MISS / BYPASS).
FILE:templates/cache-review-report.md
# Báo Cáo Đánh Giá Bộ Nhớ Đệm HTTP - <site / service> - <date>
**Phạm vi:** <URLs or HAR file> **CDN:** <name or none> **Thu thập bằng:** <curl -sI / DevTools / HAR>
## Tóm tắt
- Số phản hồi đã đánh giá: <n> Mức Cao: <n> Mức Trung bình: <n> Mức Thấp: <n>
- Rủi ro lớn nhất: <ví dụ: API cá nhân bị lưu đệm trên CDN>
- Cải thiện mang lại hiệu quả lớn nhất: <ví dụ: lưu đệm một năm cho các tài sản tĩnh có gắn mã băm>
## Đối chiếu hiện tại và khuyến nghị
| Tài nguyên | Loại | Cache-Control hiện tại | TTL Trình duyệt / CDN | Cờ cảnh báo | Khuyến nghị |
|---|---|---|---|---|---|
| | | | | | |
## Danh sách cần khắc phục
| Ưu tiên | Thay đổi cần làm | Vị trí sửa (máy chủ, framework, quy tắc CDN) | Người phụ trách | Hoàn thành |
|---|---|---|---|---|
| 1 | | | | [ ] |
## Xác minh
- [ ] Chạy lại lệnh `python3 scripts/cache_audit.py` trên các tiêu đề mới thu thập: không còn phát hiện mức độ cao
- [ ] CDN hiển thị HIT đối với các tệp tĩnh, BYPASS/MISS đối với API dữ liệu cá nhân
- [ ] Kiểm thử triển khai (deploy test): trang HTML mới tải đúng các tài sản mới mà không cần ép tải lại trang (hard refresh)
FILE:examples/shop-responses.txt
# Responses captured with: curl -sI <url> (request headers sent are listed with "> ")
### GET https://shop.example/
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=86400
date: Wed, 21 Oct 2026 07:00:00 GMT
### GET https://shop.example/assets/app.3f9a1c7b.js
HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=3600
etag: "3f9a1c7b"
### GET https://shop.example/assets/vendor.81d2e0aa.css
HTTP/2 200
content-type: text/css
cache-control: public, max-age=31536000, immutable
etag: "81d2e0aa"
### GET https://shop.example/js/widget.js
HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=31536000, immutable
### GET https://shop.example/img/hero.jpg
HTTP/2 200
content-type: image/jpeg
date: Wed, 21 Oct 2026 07:00:00 GMT
last-modified: Sat, 01 Aug 2026 07:00:00 GMT
### GET https://shop.example/api/cart
> Cookie: session=abc123
HTTP/2 200
content-type: application/json
cache-control: public, s-maxage=120
vary: Cookie
### POST https://shop.example/login
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=600
set-cookie: session=xyz789; HttpOnly; Secure; SameSite=Lax
### GET https://shop.example/api/products?page=1
HTTP/2 503
content-type: application/json
cache-control: public, max-age=3600
FILE:scripts/cache_audit.py
#!/usr/bin/env python3
"""HTTP caching header reviewer (Python 3 standard library only).
Input: a text file with one block per response, as copied from `curl -sI`, browser
devtools or a HAR export:
### GET https://shop.example/assets/app.3f9a1c7b.js
> Authorization: Bearer ... (optional request headers start with "> ")
HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=31536000, immutable
etag: "3f9a1c7b"
Blank lines and lines starting with "#" (but not "###") are ignored.
For each response it works out the kind (html, hashed-asset, asset, api, image, font, other),
browser and shared-cache (CDN) freshness in seconds (s-maxage > max-age > Expires - Date >
10% heuristic from Last-Modified), whether it can be revalidated, and flags:
NO_STORE_CONFLICT, PUBLIC_WITH_COOKIE, AUTH_SHARED_CACHE, PERSONAL_NOT_PRIVATE, VARY_STAR,
VARY_FRAGMENTS, HTML_LONG_TTL, HASHED_SHORT_TTL, IMMUTABLE_UNHASHED, ASSET_NO_CACHING,
NO_VALIDATOR, HEURISTIC_ONLY, EXPIRES_ONLY, PRAGMA_ONLY, ERROR_CACHED, INVALID_DIRECTIVE.
It suggests a Cache-Control value per kind.
Usage:
python3 cache_audit.py responses.txt
python3 cache_audit.py responses.txt --json
python3 cache_audit.py --self-test
Exit code: 0 OK, 1 high-severity flags found, 2 bad input.
"""
import argparse
import email.utils
import json
import re
import sys
KNOWN = {"max-age", "s-maxage", "no-cache", "no-store", "no-transform", "must-revalidate",
"proxy-revalidate", "must-understand", "private", "public", "immutable",
"stale-while-revalidate", "stale-if-error"}
NEEDS_INT = {"max-age", "s-maxage", "stale-while-revalidate", "stale-if-error"}
HASH_RE = re.compile(r"[.\-_]([0-9a-f]{8,}|[0-9A-Za-z_-]{8,}(?=\.[a-z0-9]+$))\.[a-z0-9]+$")
SEVERITY = {"NO_STORE_CONFLICT": "low", "PUBLIC_WITH_COOKIE": "high", "AUTH_SHARED_CACHE": "high",
"PERSONAL_NOT_PRIVATE": "medium", "VARY_STAR": "medium", "VARY_FRAGMENTS": "medium",
"HTML_LONG_TTL": "high", "HASHED_SHORT_TTL": "medium", "IMMUTABLE_UNHASHED": "high",
"ASSET_NO_CACHING": "medium", "NO_VALIDATOR": "low", "HEURISTIC_ONLY": "medium",
"EXPIRES_ONLY": "low", "PRAGMA_ONLY": "low", "ERROR_CACHED": "medium",
"INVALID_DIRECTIVE": "medium"}
RECOMMEND = {
"html": "no-cache (plus ETag or Last-Modified), or max-age=0, must-revalidate",
"hashed-asset": "public, max-age=31536000, immutable",
"asset": "public, max-age=3600, stale-while-revalidate=86400 (or add a content hash to the file name)",
"image": "public, max-age=86400, stale-while-revalidate=604800",
"font": "public, max-age=31536000, immutable (if the URL is versioned)",
"api": "private, no-cache for user data; public, max-age=60, s-maxage=300 only for truly public data",
"personal": "private, no-cache (or no-store for sensitive data)",
"other": "set an explicit Cache-Control",
}
class CacheError(ValueError):
pass
def parse_cc(value):
"""Return (directives dict, problems list)."""
d, problems = {}, []
for part in [p.strip() for p in value.split(",") if p.strip()]:
k, eq, v = part.partition("=")
k = k.strip().lower()
v = v.strip().strip('"')
if k not in KNOWN:
problems.append(f"unknown directive {k!r}")
continue
if k in NEEDS_INT:
if not re.fullmatch(r"\d+", v or ""):
problems.append(f"{k} needs a whole number of seconds, got {v!r}")
continue
d[k] = int(v)
else:
d[k] = True if not eq else v
return d, problems
def parse_blocks(text):
blocks, cur = [], None
for raw in text.splitlines():
line = raw.rstrip("\r")
if line.startswith("###"):
cur = {"label": line[3:].strip(), "status": None, "headers": {}, "request": {}}
blocks.append(cur)
continue
if not line.strip() or line.lstrip().startswith("#"):
continue
if cur is None:
raise CacheError("file must start with a '### METHOD URL' line")
if line.startswith(">"):
k, _, v = line[1:].strip().partition(":")
cur["request"][k.strip().lower()] = v.strip()
continue
m = re.match(r"HTTP/[\d.]+\s+(\d{3})", line)
if m:
cur["status"] = int(m.group(1))
continue
k, colon, v = line.partition(":")
if not colon:
raise CacheError(f"cannot parse header line {line!r} in block {cur['label']!r}")
k = k.strip().lower()
if k in cur["headers"] and k in ("cache-control", "vary"):
cur["headers"][k] += ", " + v.strip()
elif k == "set-cookie":
cur["headers"].setdefault(k, []).append(v.strip())
else:
cur["headers"][k] = v.strip()
if not blocks:
raise CacheError("no response blocks found")
return blocks
def url_of(label):
parts = label.split()
return parts[-1] if parts else ""
def kind_of(url, ctype):
path = re.sub(r"[?#].*$", "", url)
name = path.rsplit("/", 1)[-1].lower()
ctype = ctype.lower()
hashed = bool(HASH_RE.search(name)) and bool(re.search(r"\d", name))
if "text/html" in ctype or name.endswith(".html") or (not ctype and "." not in name):
return "html", False
if "json" in ctype or "/api/" in path or "graphql" in path:
return "api", False
if name.endswith((".woff2", ".woff", ".ttf", ".otf")) or "font" in ctype:
return "font", hashed
if ctype.startswith("image/") or name.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico")):
return ("hashed-asset" if hashed else "image"), hashed
if name.endswith((".js", ".mjs", ".css", ".map", ".wasm")) or "javascript" in ctype or "text/css" in ctype:
return ("hashed-asset" if hashed else "asset"), hashed
return "other", hashed
def http_date(v):
try:
return email.utils.parsedate_to_datetime(v)
except (TypeError, ValueError, IndexError):
return None
def review(block):
h, req = block["headers"], block["request"]
status = block["status"] or 200
url = url_of(block["label"])
kind, hashed = kind_of(url, h.get("content-type", ""))
cc, problems = parse_cc(h.get("cache-control", ""))
flags = []
def flag(code, detail):
flags.append({"code": code, "severity": SEVERITY[code], "detail": detail})
for p in problems:
flag("INVALID_DIRECTIVE", p)
cookies = h.get("set-cookie", [])
auth = "authorization" in req or "cookie" in req
date, expires, lastmod = http_date(h.get("date")), h.get("expires"), http_date(h.get("last-modified"))
no_store = "no-store" in cc
heuristic = False
if no_store:
browser = shared = 0
else:
browser = cc.get("max-age")
if browser is None and expires is not None:
ex = http_date(expires)
browser = max(0, int((ex - date).total_seconds())) if ex and date else 0
if browser is None and "no-cache" not in cc and lastmod and date and status in (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501):
browser = max(0, int((date - lastmod).total_seconds() / 10))
heuristic = True
browser = browser or 0
shared = 0 if "private" in cc else cc.get("s-maxage", browser)
if "no-cache" in cc:
browser_eff, shared_eff = 0, 0
else:
browser_eff, shared_eff = browser, shared
validator = "etag" in h or "last-modified" in h
personal = bool(cookies) or auth
if no_store and any(k in cc for k in ("max-age", "s-maxage", "public", "immutable")):
flag("NO_STORE_CONFLICT", "no-store overrides the other directives; remove the extras so intent is clear")
if cookies and ("public" in cc or "s-maxage" in cc) and not no_store:
flag("PUBLIC_WITH_COOKIE", "response sets a cookie but allows shared caches; a CDN may serve one user's cookie to others")
if auth and shared > 0 and ("public" in cc or "s-maxage" in cc) and kind in ("api", "html", "other"):
flag("AUTH_SHARED_CACHE", "authenticated request with a response that shared caches may store; personal data can leak across users")
elif personal and kind in ("api", "html", "other") and not no_store and "private" not in cc \
and not any(f["code"] == "PUBLIC_WITH_COOKIE" for f in flags):
flag("PERSONAL_NOT_PRIVATE", "personalized response without private or no-store")
vary = [v.strip().lower() for v in h.get("vary", "").split(",") if v.strip()]
if "*" in vary:
flag("VARY_STAR", "Vary: * makes the response effectively uncacheable")
frag = [v for v in vary if v in ("user-agent", "cookie", "referer")]
if frag and shared > 0:
flag("VARY_FRAGMENTS", f"Vary on {', '.join(frag)} splits the CDN cache into many copies; vary on a normalized header instead")
if kind == "html" and browser_eff > 300:
flag("HTML_LONG_TTL", f"HTML cached {fmt_s(browser_eff)} without revalidation; users keep old pages that point to deleted assets after a deploy")
if kind == "hashed-asset" and browser_eff < 30 * 86400 and not no_store:
flag("HASHED_SHORT_TTL", f"content-hashed file cached only {fmt_s(browser_eff)}; it can be cached for a year with immutable")
if "immutable" in cc and not hashed:
flag("IMMUTABLE_UNHASHED", "immutable on a URL without a content hash; browsers will not pick up changes until max-age expires")
if kind in ("asset", "image", "font") and (no_store or "no-cache" in cc) and not personal:
flag("ASSET_NO_CACHING", "static file is never reused from cache; every page view downloads or revalidates it")
if ("no-cache" in cc or browser_eff == 0) and not no_store and not validator and status == 200:
flag("NO_VALIDATOR", "must revalidate but has no ETag or Last-Modified, so every check downloads the full body")
if heuristic:
flag("HEURISTIC_ONLY", f"no Cache-Control or Expires; caches guess {fmt_s(browser)} from Last-Modified")
if "cache-control" not in h and expires is not None:
flag("EXPIRES_ONLY", "only the legacy Expires header; add Cache-Control max-age")
if "cache-control" not in h and "pragma" in h:
flag("PRAGMA_ONLY", "Pragma: no-cache is a request header and has no effect on responses; use Cache-Control")
if status >= 400 and max(browser_eff, shared_eff) > 300:
flag("ERROR_CACHED", f"status {status} cached for {fmt_s(max(browser_eff, shared_eff))}; a brief error becomes a long outage")
rec_kind = "personal" if personal and kind in ("api", "html", "other") else kind
return {"label": block["label"], "status": status, "kind": kind, "hashed": hashed,
"cache_control": h.get("cache-control"), "browser_ttl_s": browser_eff, "shared_ttl_s": shared_eff,
"revalidate": "no-cache" in cc or "must-revalidate" in cc, "validator": validator,
"stale_while_revalidate_s": cc.get("stale-while-revalidate"), "flags": flags,
"recommended": RECOMMEND[rec_kind]}
def fmt_s(s):
if s is None:
return "-"
for unit, n in (("d", 86400), ("h", 3600), ("m", 60)):
if s >= n:
v = s / n
return f"{v:.0f}{unit}" if v == int(v) else f"{v:.1f}{unit}"
return f"{s}s"
def audit(text):
items = [review(b) for b in parse_blocks(text)]
return {"items": items, "high_flags": sum(1 for i in items for f in i["flags"] if f["severity"] == "high")}
def to_markdown(r):
out = ["# HTTP cache header review", "",
"| Response | Status | Kind | Browser TTL | CDN TTL | Revalidate | Validator | Flags |",
"|---|---|---|---|---|---|---|---|"]
for i in r["items"]:
out.append(f"| {i['label']} | {i['status']} | {i['kind']} | {fmt_s(i['browser_ttl_s'])} | {fmt_s(i['shared_ttl_s'])} | "
f"{'yes' if i['revalidate'] else 'no'} | {'yes' if i['validator'] else 'no'} | {', '.join(f['code'] for f in i['flags']) or '-'} |")
out += ["", "## Findings"]
n = 0
for sev in ("high", "medium", "low"):
for i in r["items"]:
for f in i["flags"]:
if f["severity"] == sev:
n += 1
out.append(f"- **{sev.upper()} {f['code']}** {i['label']}: {f['detail']}")
if not n:
out.append("- No findings.")
out += ["", "## Suggested Cache-Control"]
for i in r["items"]:
if i["flags"]:
out.append(f"- {i['label']}: `{i['recommended']}` (now: `{i['cache_control'] or 'none'}`)")
return "\n".join(out)
def self_test():
cases = []
def ok(name, cond):
cases.append((name, bool(cond)))
def one(url, *lines):
return review(parse_blocks("### GET " + url + "\nHTTP/1.1 200 OK\n" + "\n".join(lines))[0])
def codes(item):
return {f["code"] for f in item["flags"]}
ok("parse max-age", parse_cc("public, max-age=600")[0] == {"public": True, "max-age": 600})
ok("bad max-age reported", parse_cc("max-age=10m")[1])
ok("unknown directive reported", parse_cc("maxage=60")[1])
ok("hashed js detected", kind_of("https://x/app.3f9a1c7b.js", "") == ("hashed-asset", True))
ok("unhashed css", kind_of("https://x/styles.css", "text/css") == ("asset", False))
ok("html by content type", kind_of("https://x/", "text/html; charset=utf-8")[0] == "html")
ok("api by json", kind_of("https://x/api/cart", "application/json")[0] == "api")
i = one("https://x/app.3f9a1c7b.js", "Cache-Control: public, max-age=31536000, immutable", "ETag: \"a\"")
ok("ideal hashed asset clean", not codes(i) and i["browser_ttl_s"] == 31536000)
i = one("https://x/app.3f9a1c7b.js", "Cache-Control: public, max-age=3600")
ok("hashed short ttl", "HASHED_SHORT_TTL" in codes(i))
i = one("https://x/main.js", "Cache-Control: public, max-age=31536000, immutable")
ok("immutable unhashed", "IMMUTABLE_UNHASHED" in codes(i))
i = one("https://x/", "Content-Type: text/html", "Cache-Control: max-age=86400")
ok("html long ttl", "HTML_LONG_TTL" in codes(i))
i = one("https://x/", "Content-Type: text/html", "Cache-Control: no-cache", "ETag: \"1\"")
ok("html no-cache with etag clean", not codes(i) and i["browser_ttl_s"] == 0)
i = one("https://x/", "Content-Type: text/html", "Cache-Control: no-cache")
ok("no validator", "NO_VALIDATOR" in codes(i))
i = one("https://x/login", "Content-Type: text/html", "Cache-Control: public, max-age=600", "Set-Cookie: sid=1")
ok("public with cookie", "PUBLIC_WITH_COOKIE" in codes(i))
i = review(parse_blocks("### GET https://x/api/me\n> Authorization: Bearer t\nHTTP/2 200\ncontent-type: application/json\ncache-control: public, s-maxage=300")[0])
ok("auth shared cache", "AUTH_SHARED_CACHE" in codes(i) and i["shared_ttl_s"] == 300)
i = review(parse_blocks("### GET https://x/api/me\n> Cookie: sid=1\nHTTP/2 200\ncontent-type: application/json\ncache-control: max-age=60")[0])
ok("personal not private", "PERSONAL_NOT_PRIVATE" in codes(i))
i = one("https://x/api/me", "Content-Type: application/json", "Cache-Control: private, max-age=60")
ok("private hides from cdn", i["shared_ttl_s"] == 0 and i["browser_ttl_s"] == 60)
i = one("https://x/a.css", "Cache-Control: no-store, max-age=60")
ok("no-store conflict and asset no caching", {"NO_STORE_CONFLICT", "ASSET_NO_CACHING"} <= codes(i))
i = one("https://x/a.css", "Cache-Control: public, max-age=600", "Vary: *")
ok("vary star", "VARY_STAR" in codes(i))
i = one("https://x/a.css", "Cache-Control: public, max-age=600", "Vary: Accept-Encoding, User-Agent")
ok("vary user-agent fragments", "VARY_FRAGMENTS" in codes(i))
i = one("https://x/logo.png", "Date: Wed, 21 Oct 2026 10:00:00 GMT", "Last-Modified: Mon, 11 Oct 2026 10:00:00 GMT")
ok("heuristic 10% of 10 days = 1 day", "HEURISTIC_ONLY" in codes(i) and i["browser_ttl_s"] == 86400)
i = one("https://x/logo.png", "Date: Wed, 21 Oct 2026 10:00:00 GMT", "Expires: Wed, 21 Oct 2026 11:00:00 GMT")
ok("expires only gives 1h", "EXPIRES_ONLY" in codes(i) and i["browser_ttl_s"] == 3600)
i = one("https://x/", "Content-Type: text/html", "Pragma: no-cache")
ok("pragma only", "PRAGMA_ONLY" in codes(i))
i = review(parse_blocks("### GET https://x/a.css\nHTTP/1.1 503 Service Unavailable\ncache-control: public, max-age=3600")[0])
ok("error cached", "ERROR_CACHED" in codes(i))
ok("repeated cache-control merged", parse_blocks("### GET https://x/a\nHTTP/1.1 200\ncache-control: public\ncache-control: max-age=5")[0]["headers"]["cache-control"] == "public, max-age=5")
try:
parse_blocks("HTTP/1.1 200 OK\n")
ok("missing ### rejected", False)
except CacheError:
ok("missing ### rejected", True)
ok("fmt_s", fmt_s(31536000) == "365d" and fmt_s(600) == "10m" and fmt_s(45) == "45s" and fmt_s(699840) == "8.1d")
passed = sum(c for _, c in cases)
for name, c in cases:
print(("PASS " if c else "FAIL ") + name)
print(f"{passed}/{len(cases)} passed")
return 0 if passed == len(cases) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="Review HTTP caching headers.")
ap.add_argument("file", nargs="?", help="responses text file ('-' for stdin)")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not a.file:
ap.error("responses file required (or --self-test)")
try:
text = sys.stdin.read() if a.file == "-" else open(a.file, encoding="utf-8").read()
r = audit(text)
except (OSError, CacheError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
print(json.dumps(r, indent=2) if a.json else to_markdown(r))
return 1 if r["high_flags"] else 0
if __name__ == "__main__":
sys.exit(main())Các quy ước mang tính định hướng rõ rệt khi sử dụng Redis làm bộ nhớ đệm (cache): khóa (keys), thời gian sống (TTLs), cơ chế vô hiệu hóa (invalidation) và vận hành.
# Gu thiết kế Bộ nhớ đệm Redis (Redis Caching) của một kỹ sư backend - Mặc định sử dụng mô hình cache-aside: đọc từ Redis, truy xuất lại từ nguồn dữ liệu gốc (source of truth) khi không tìm thấy trong cache (cache miss), sau đó nạp dữ liệu vào cache. Độ tin cậy: 0.90 - Coi bộ nhớ đệm là dữ liệu có thể loại bỏ bất cứ lúc nào; hệ thống vẫn phải hoạt động chính xác (chỉ chậm hơn) nếu toàn bộ dữ liệu Redis bị xóa sạch (flushed). Độ tin cậy: 0.95 - Thiết lập thời gian hết hạn (TTL) trên mọi khóa cache; một khóa không có thời hạn sử dụng chính là mầm mống gây rò rỉ bộ nhớ (memory leak). Độ tin cậy: 0.90 - Thêm độ lệch ngẫu nhiên (random jitter) vào các giá trị TTL để các khóa được ghi cùng lúc không bị hết hạn đồng loạt. Độ tin cậy: 0.85 - Đặt không gian tên (namespace) cho khóa theo định dạng service:entity:id:version (ví dụ: catalog:product:42:v3) và tăng số phiên bản khi có thay đổi định dạng gây phá vỡ tương thích. Độ tin cậy: 0.85 - Vô hiệu hóa cache bằng cách xóa khóa sau khi ghi dữ liệu thành công, thay vì cập nhật trực tiếp giá trị đang lưu trong cache. Độ tin cậy: 0.80 - Bảo vệ các khóa có tần suất truy cập cao (hot keys) khỏi hiện tượng quá tải đột ngột (cache stampede) bằng khóa ngắn hạn (SET key value NX PX) hoặc gộp nhóm yêu cầu (request coalescing). Độ tin cậy: 0.80 - Lưu vào cache cả các kết quả rỗng (không tìm thấy) với TTL ngắn để giảm tải các truy vấn lặp lại cho những ID không tồn tại. Độ tin cậy: 0.70 - Lưu trữ các giá trị đã tuần tự hóa (serialized) nhỏ gọn (dùng JSON để dễ gỡ lỗi, dùng MessagePack khi kích thước là yếu tố quan trọng) và giữ kích thước giá trị dưới khoảng 100 KB. Độ tin cậy: 0.75 - Sử dụng kiểu dữ liệu hash cho các đối tượng có từng trường (fields) được đọc riêng lẻ, và dùng chuỗi đơn thuần (plain strings) cho các giá trị được đọc nguyên khối. Độ tin cậy: 0.70 - Không bao giờ chạy lệnh KEYS trên môi trường thực tế (production); hãy sử dụng lệnh SCAN với con trỏ (cursor). Độ tin cậy: 0.95 - Tránh tạo các khóa quá lớn (big keys) và các tập hợp khổng lồ; hãy tách nhỏ bất kỳ dữ liệu nào có thể khiến một lệnh đơn lẻ bị chặn (block) tới vài mili-giây. Độ tin cậy: 0.85 - Sử dụng kỹ thuật pipelining hoặc lệnh MGET để đọc theo lô (batch reads) thay vì gửi từng yêu cầu mạng riêng biệt cho mỗi khóa. Độ tin cậy: 0.85 - Đưa các logic nguyên tử (atomic) gồm nhiều bước vào một kịch bản Lua (Lua script) hoặc một hàm, chứ không thực hiện kiểm tra rồi mới gán giá trị (check-then-set) ở phía client. Độ tin cậy: 0.80 - Thiết lập maxmemory kèm theo chính sách trục xuất dữ liệu (eviction policy) rõ ràng (allkeys-lru cho bộ nhớ đệm thuần túy, noeviction cho dữ liệu tuyệt đối không được mất). Độ tin cậy: 0.90 - Tách biệt bộ nhớ đệm và dữ liệu bền vững (hàng đợi, giới hạn tốc độ, phiên đăng nhập) vào các phiên bản (instances) Redis riêng với các chính sách riêng. Độ tin cậy: 0.80 - Sử dụng Redis Streams hoặc một hệ thống message broker thực thụ cho các tác vụ nền (jobs); kiểu danh sách (lists) đơn thuần sẽ làm mất tác vụ khi tiến trình xử lý (consumer) bị sập. Độ tin cậy: 0.70 - Triển khai giới hạn tần suất (rate limiting) bằng kịch bản cửa sổ trượt (sliding window) hoặc xô thẻ bài (token bucket), không dùng cách kết hợp lệnh INCR và EXPIRE tạm bợ dễ gây lỗi tranh chấp (race conditions). Độ tin cậy: 0.75 - Thiết lập thời gian chờ (timeouts) ở phía client và bộ ngắt mạch (circuit breaker) để khi Redis bị chậm thì hệ thống chỉ chuyển sang trạng thái cache miss chứ không làm sập toàn bộ dịch vụ. Độ tin cậy: 0.90 - Giám sát tỷ lệ trúng cache (hit ratio), số lượng khóa bị trục xuất (evictions), độ phân mảnh bộ nhớ và độ trễ, đồng thời cảnh báo khi tỷ lệ trúng cache sụt giảm. Độ tin cậy: 0.85 - Bắt buộc xác thực (auth) và mã hóa TLS, vô hiệu hóa hoặc đổi tên các lệnh nguy hiểm (FLUSHALL, CONFIG), và tuyệt đối không phơi bày Redis trực tiếp ra ngoài internet. Độ tin cậy: 0.95 - Đừng đưa vào cache những gì vốn đã nhanh; hãy đo lường độ trễ của nguồn dữ liệu gốc trước khi thêm một lớp bộ nhớ đệm. Độ tin cậy: 0.80