Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Biết tạo redirect response với
axum::response::Redirect::to(url)và 4 builder method ánh xạ 4 status code (301/302/303/307/308). - Phân biệt được permanent vs temporary redirect (301/308 vs 302/307) — chiều quyết định liên quan đến client cache.
- Phân biệt được preserve method vs allow change (307/308 vs 301/302/303) — chiều quyết định liên quan đến POST body.
- Sử dụng
tower-httpServeDircho static folder,ServeFilecho single file (cần featurefs). - Cấu hình cache header cho static asset:
Cache-Control: public, max-age=31536000, immutableproduction vsno-cachedev. - Hiểu Shop API CDN strategy: asset static (product image, JS, CSS) đẩy CDN, REST API KHÔNG tự host.
Redirect Cơ Bản: Redirect::to(url)
axum::response::Redirect là response type built-in cho redirect. Bốn builder method static tạo instance, sai khác duy nhất ở status code emit:
use axum::response::Redirect;
// 1. Redirect::permanent — 308 Permanent Redirect
async fn migrated_v0_to_v1() -> Redirect {
Redirect::permanent("/api/v1/products")
}
// 2. Redirect::to — 303 See Other (POST-redirect-GET)
async fn login_success() -> Redirect {
Redirect::to("/dashboard")
}
// 3. Redirect::temporary — 307 Temporary Redirect
async fn maintenance() -> Redirect {
Redirect::temporary("/maintenance.html")
}
// 4. Redirect::found — 302 Found (legacy)
async fn legacy_handler() -> Redirect {
Redirect::found("/somewhere")
}
Bốn method khác nhau ở status code: permanent → 308, to → 303, temporary → 307, found → 302. Lưu ý quan trọng: Redirect::permanent trong axum 0.8 trả về 308 Permanent Redirect chứ KHÔNG phải 301 Moved Permanently — khác trực giác từ tên Vietnamese "permanent" đầu tiên gặp. Tham khảo docs.rs/axum/latest/axum/response/struct.Redirect.html.
Cả 4 method đều build response với header Location: <url> tự động + body rỗng. Browser hoặc HTTP client đọc Location header, gửi request mới đến URL đó, lặp cho đến khi hit response non-3xx (default tối đa 10 redirect, qua đó client ngắt với error "too many redirects" tránh loop vô hạn).
Tham số url chấp nhận impl Into<String> — bạn truyền &str literal, String owned, hoặc kết quả format!(...). URL có thể là path relative (/api/v1/products) hoặc absolute URL (https://cdn.shop.com/assets/x.webp) — browser xử lý cả 2.
Phân Biệt 5 Status Code Redirect
RFC 9110 mục 15.4 định nghĩa 5 status code redirect chính. Hai chiều quyết định: (a) permanent vs temporary — client cache vĩnh viễn hay không; (b) preserve method/body vs allow change — request tiếp theo dùng method gốc hay client được phép đổi sang GET.
┌──────┬───────────────────────┬───────────┬─────────────────┐
│ Code │ Tên │ Permanent │ Preserve method │
├──────┼───────────────────────┼───────────┼─────────────────┤
│ 301 │ Moved Permanently │ Yes │ No (có thể đổi) │
│ 302 │ Found │ No │ No (có thể đổi) │
│ 303 │ See Other │ No │ No (luôn GET) │
│ 307 │ Temporary Redirect │ No │ Yes │
│ 308 │ Permanent Redirect │ Yes │ Yes │
└──────┴───────────────────────┴───────────┴─────────────────┘
Chi tiết từng code:
- 301 Moved Permanently — resource đã chuyển vĩnh viễn. Client cache mạnh (browser nhớ luôn, lần sau tự gửi tới URL mới không hỏi server). Method có thể đổi: lịch sử HTTP/1.0 nhiều browser tự chuyển POST → GET sau 301 dù RFC khuyến nghị giữ method (lý do RFC bổ sung 308 tách bạch). Use case: URL renamed permanently, SEO benefit search engine update index.
- 302 Found — temporary, method có thể đổi. Semantic mơ hồ từ HTTP/1.0 (tên gốc "Moved Temporarily"), browser thực tế convert POST → GET. RFC 9110 hiện nay khuyến nghị dùng 303 hoặc 307 thay thế — 302 giữ cho backward compat legacy.
- 303 See Other — tell client GET resource khác (luôn dùng GET cho request tiếp theo bất kể method gốc). Use case kinh điển: POST-redirect-GET pattern — sau khi POST
/loginthành công, server redirect 303 tới GET/dashboardđể tránh resubmit form khi user refresh. - 307 Temporary Redirect — temporary, PRESERVE method + body. Client gửi lại request với chính method gốc (POST → POST, PUT → PUT). Use case: temporary maintenance redirect — bạn muốn POST đang in-flight không bị convert sang GET làm mất body.
- 308 Permanent Redirect — permanent, PRESERVE method + body. Khắc phục ambiguous của 301. Use case: domain migration, API endpoint move (
/api/v0/products→/api/v1/products) — client gửi POST/PUT tiếp tục được preserve.
Shop API decision lock cho từng tình huống:
- 301 — chỉ dùng khi rename URL vĩnh viễn cho asset SEO matter (rare, hiện không có endpoint nào). Cẩn thận: browser cache 301 mạnh, sai một lần khó undo (user phải clear cache).
- 303 — admin form submit POST-redirect-GET (admin dashboard B105 sau khi tạo product redirect tới detail page GET).
- 307 — temporary maintenance redirect (đẩy traffic sang status page) + shortcut REST endpoint → CDN URL (giữ method gốc cho cache invalidation logic phía CDN).
- 308 — API versioning migration
/api/v0/*→/api/v1/*khi sunset version cũ.
Decision Tree: Permanent + Method Preserve
Decision tree chọn đúng status code khi bạn cần redirect:
Permanent redirect?
Yes → Preserve method? → Yes → 308 (Redirect::permanent)
→ No → 301 (chưa có wrapper trong axum)
No → Preserve method? → Yes → 307 (Redirect::temporary)
→ No → 303 (Redirect::to)
Lưu ý: KHÔNG dùng 302 nữa,
RFC 9110 khuyến nghị 303 hoặc 307
axum 0.8 Redirect chỉ expose 4 builder cho 4 code: 308, 303, 307, 302. Để emit 301 raw, bạn tự build response qua tuple (StatusCode::MOVED_PERMANENTLY, [(header::LOCATION, "/new-url")]):
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
// Emit 301 raw — axum::response::Redirect không có builder cho 301
async fn rename_permanent() -> impl IntoResponse {
(
StatusCode::MOVED_PERMANENTLY,
[(header::LOCATION, "/new-permanent-url")],
)
}
Pitfall lock: dùng 301 sai chỗ — browser cache 301 trong nhiều ngày tới vô thời hạn (theo cache header, có khi không hết cho đến khi user clear). Nếu bạn 301 từ /api/v1/users sang URL sai do typo, user truy cập sẽ tự chuyển sang URL sai mãi mà không có cách reset từ phía server. Quy tắc: chỉ 301 khi CHẮC CHẮN 100% URL mới đúng + vĩnh viễn; nghi ngờ thì 307 hoặc 308 (308 cache yếu hơn 301 ở một số browser implementation, dễ rollback hơn).
Mẫu handler cho từng status code Shop API sẽ dùng:
use axum::response::Redirect;
// 308 — API versioning migration (permanent, preserve method)
async fn migrate_v0_to_v1() -> Redirect {
Redirect::permanent("/api/v1/products")
}
// 307 — temporary maintenance (temporary, preserve method)
async fn maintenance_redirect() -> Redirect {
Redirect::temporary("/maintenance.html")
}
// 303 — POST-redirect-GET (temporary, force GET)
async fn admin_create_product_success() -> Redirect {
Redirect::to("/admin/products")
}
Serve Static: ServeDir & ServeFile
tower-http cung cấp 2 service cho serve static file:
ServeDir— serve toàn bộ folder, request path map sang file path trên disk. VdServeDir::new("public")mount với prefix/static, requestGET /static/style.css→ đọc file./public/style.csstrả về với MIME type tự detect từ extension.ServeFile— serve single file at path cố định. VdServeFile::new("assets/favicon.ico")mount tại route/favicon.ico, mọi request đến route đó đều trả về cùng file.
Cả 2 đều là tower::Service chứ không phải axum::handler::Handler — mount qua Router::nest_service hoặc Router::route_service thay vì nest/route:
use axum::Router;
use tower_http::services::{ServeDir, ServeFile};
// Mount folder static qua nest_service (NOT nest)
let static_router = Router::new()
.nest_service("/static", ServeDir::new("public"))
.route_service("/favicon.ico", ServeFile::new("assets/favicon.ico"));
Phân biệt nest vs nest_service: nest nhận Router con (đăng ký nhiều route qua axum API), nest_service nhận thẳng tower::Service (single service xử lý mọi request đến prefix). Tương tự route vs route_service cho single path.
Để dùng ServeDir và ServeFile bạn cần thêm feature fs vào tower-http. Workspace dependencies hiện tại của Shop API (lock B10) đã có trace + cors + compression-gzip, cần extend thêm fs khi thực sự serve static:
# File: Cargo.toml (workspace root)
[workspace.dependencies]
tower-http = { version = "0.6", features = [
"trace",
"cors",
"compression-gzip",
"fs", # NEW — cho ServeDir + ServeFile
] }
Shop API hiện tại chưa add feature fs vì lock B27 sẽ áp dụng CDN strategy không tự serve static (chi tiết bước 7). Feature này note add khi cần thực tế (vd serve SPA frontend bundle nếu deploy monorepo).
Behavior khác cần biết của ServeDir:
- Tự detect MIME type từ file extension qua
mime_guesscrate (.css→text/css,.js→application/javascript,.webp→image/webp). - Set
ETagheader tự động dựa trên file mtime + size — browser conditional GET vớiIf-None-Match→ 304 Not Modified tiết kiệm bandwidth. - Hỗ trợ pre-compressed file qua method chain
.precompressed_gzip(),.precompressed_br()— nếu folder cóstyle.css.brsẵn, request vớiAccept-Encoding: brtrả file đã nén tránh nén lại runtime. - Không tự set
Cache-Control— bạn phải wrap layer thêm (bước 6).
Cache Header Cho Static Files
ServeDir mặc định KHÔNG set Cache-Control — browser fallback heuristic (thường cache vài phút theo Last-Modified). Production cần chiến lược rõ ràng:
- Production — asset có hash trong filename (
app.abc123.js,style.def456.css): setCache-Control: public, max-age=31536000, immutable— 1 năm + flagimmutablebáo browser không re-validate. Khi build mới sinh hash khác → filename khác → URL khác → cache hit cũ không ảnh hưởng version mới (cache busting qua filename). - Production — file không hash (
index.html):Cache-Control: public, max-age=300hoặcno-cacheđể browser luôn re-validate, đảm bảo deploy mới user thấy ngay. - Dev:
Cache-Control: no-cacheđể browser luôn fetch — mỗi save file thay đổi reflect ngay trong tab đang mở (HMR-like behavior cho static file).
Pattern set Cache-Control qua SetResponseHeaderLayer wrap ServeDir:
use axum::http::{header, HeaderValue};
use axum::Router;
use tower_http::services::ServeDir;
use tower_http::set_header::SetResponseHeaderLayer;
// Static service với pre-compressed asset support
let static_service = ServeDir::new("public")
.precompressed_gzip()
.precompressed_br();
// Wrap layer set Cache-Control cho mọi response
let static_router = Router::new()
.nest_service("/static", static_service)
.layer(SetResponseHeaderLayer::overriding(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=31536000, immutable"),
));
SetResponseHeaderLayer::overriding ghi đè header đã có (nếu inner service set Cache-Control trước, layer này override). Biến thể ::if_not_present chỉ set khi chưa có — phù hợp khi inner service đã quyết định cache strategy per file.
Combo với ETag mà ServeDir set tự động: browser gửi If-None-Match: "abc123" trong request tiếp theo, server compare ETag, match → trả 304 Not Modified body rỗng (tiết kiệm bandwidth). Cache-Control max-age dài + ETag conditional GET là combo tối ưu cho static asset production.
Tham khảo: docs.rs/tower-http/latest/tower_http/set_header/index.html cho đầy đủ method SetResponseHeaderLayer.
Shop API CDN Strategy
Lock B27 cho Shop API: REST API KHÔNG tự serve static asset (product image, JS bundle, CSS, font). Toàn bộ asset đẩy lên object storage (S3, Cloudflare R2) + serve qua CDN edge (CloudFront, Cloudflare CDN, Bunny CDN).
Bốn lý do:
- App server tập trung business logic — code Rust optimize cho concurrency + low-latency database access, KHÔNG cần cạnh tranh resource serve image hàng GB.
- Edge caching globally — CDN có POP (point of presence) ở hàng chục city, user Hà Nội fetch image từ POP Singapore latency ~30ms thay vì cross-Pacific tới app server US ~200ms.
- Auto scale, không tốn bandwidth backend — Black Friday traffic spike image fetch tăng 100x, CDN absorb hoàn toàn, app server không thấy load này (chỉ thấy /api/v1/products query).
- Cost — bandwidth CDN ($0.01-0.04/GB) rẻ hơn nhiều bandwidth từ datacenter compute instance ($0.09/GB AWS EC2 egress).
Exception duy nhất Shop API tự serve static: Swagger UI (HTML/CSS/JS bundled qua utoipa-swagger-ui crate lock B8) — vì cần tích hợp chặt với OpenAPI spec runtime, không upload CDN. Bundle ~500KB embed vào binary qua include_bytes! macro, serve trực tiếp từ memory, không touch disk.
Pattern Shop API code: KHÔNG add ServeDir cho /products/:slug/image — thay bằng redirect 307 tới CDN URL:
use axum::extract::Path;
use axum::response::Redirect;
// File: crates/shop-api/src/handlers/products.rs (preview G7)
async fn product_image_redirect(
Path(slug): Path<String>,
) -> Redirect {
let cdn_url = format!(
"https://cdn.shop.com/products/{}.webp",
slug,
);
Redirect::temporary(&cdn_url)
}
Lý do dùng 307 chứ không phải 301: (a) URL CDN có thể đổi khi migrate provider (CloudFront → Bunny), 307 cho phép thay đổi không bị browser cache permanent; (b) preserve method (nếu một ngày bạn thêm HEAD request từ client để check existence không tải body, 307 giữ HEAD, 301 có thể bị browser convert).
Upload flow: B247 (Group Upload) chi tiết presigned URL pattern — client upload trực tiếp từ browser lên S3 qua presigned PUT URL, app server chỉ generate URL có signature thời hạn 5 phút và lưu meta data (file path, size, mime type) vào DB. App server không proxy bytes upload, tránh bottleneck.
Apply Vào Shop API: Demo Routes
Demo 2 redirect route thực tế cho Shop API trong file routes/demo_redirect.rs — mount qua router.rs ngang hàng routes/demo_error.rs hiện có. Đây là code preview (chưa lock thay đổi workspace ở B27, chỉ minh họa pattern):
// File: crates/shop-api/src/routes/demo_redirect.rs (preview, sẽ implement khi có nhu cầu thực)
use axum::{response::Redirect, routing::get, Router};
use crate::state::AppState;
pub fn routes() -> Router<AppState> {
Router::new()
// 308 — permanent migration (preserve method + body)
.route(
"/old/products",
get(|| async { Redirect::permanent("/api/v1/products") }),
)
// 307 — temporary maintenance (preserve method + body)
.route(
"/maintenance",
get(|| async { Redirect::temporary("/maintenance.html") }),
)
}
Mount qua router.rs (lock B17):
// File: crates/shop-api/src/router.rs (preview, thêm dòng .merge())
pub fn build_router(state: AppState) -> Router {
Router::new()
.route("/", get(root))
.merge(routes::health::routes())
.merge(routes::version::routes())
.merge(routes::demo_error::routes())
.merge(routes::demo_redirect::routes()) // NEW
.with_state(state)
}
Verify qua curl flag -v in chi tiết request/response gồm status line + Location header:
# 308 Permanent Redirect (preserve method)
curl -v http://localhost:3000/old/products
# < HTTP/1.1 308 Permanent Redirect
# < location: /api/v1/products
# < content-length: 0
# < (body rỗng)
# 307 Temporary Redirect (preserve method, test với POST giữ body)
curl -v -X POST http://localhost:3000/maintenance \
-H "Content-Type: application/json" \
-d '{"foo":"bar"}'
# < HTTP/1.1 307 Temporary Redirect
# < location: /maintenance.html
# < content-length: 0
# (Browser sẽ tự POST tiếp tới /maintenance.html với cùng body)
So sánh với curl flag -L tự follow redirect:
# Follow redirect chain với -L
curl -L -v http://localhost:3000/old/products
# Trip 1: GET /old/products → 308 + Location /api/v1/products
# Trip 2: GET /api/v1/products → 200 + JSON envelope products list
# (curl tự gửi request thứ 2 đến Location URL)
Lưu ý debug: nếu test với HTTPie dùng flag --follow tương tự curl -L; Postman mặc định tự follow trừ khi bạn tắt "Automatically follow redirects" trong Settings. Khi viết integration test với axum-test (chi tiết B253), assert status code 307/308 trước khi follow để verify redirect đúng intent.
Suggested commit khi feature redirect thực được implement: B27: add demo_redirect routes (308 versioning + 307 maintenance) + document CDN strategy.
Tổng Kết
axum::response::Redirect4 builder method ánh xạ 4 status code:Redirect::permanent→ 308 (permanent, preserve method),Redirect::to→ 303 (POST-redirect-GET, force GET),Redirect::temporary→ 307 (temporary, preserve method),Redirect::found→ 302 (legacy, không khuyến nghị).- Permanent vs Temporary: 301/308 client cache vĩnh viễn, 302/303/307 không cache (mỗi request lại hit server).
- Preserve method: 307/308 giữ POST body cho request tiếp theo, 301/302/303 client có thể đổi sang GET (303 luôn GET).
- Pitfall:
Redirect::permanenttrong axum 0.8 trả 308 chứ KHÔNG phải 301 — đọcdocs.rs/axum/latest/axum/response/struct.Redirect.htmlconfirm. - Để emit 301 raw, tự build response qua tuple
(StatusCode::MOVED_PERMANENTLY, [(header::LOCATION, "/url")])— cẩn thận browser cache 301 mạnh, sai khó undo. tower-httpServeDirserve folder static +ServeFileserve single file — cần thêm featurefsvàotower-http(Shop API workspace lock B10 hiện chỉ cótrace+cors+compression-gzip, sẽ addfskhi cần thực tế).- Mount
tower::ServicequaRouter::nest_servicehoặcRouter::route_service(KHÔNG phảinest/routevốn dành choRouter+Handler). - Cache header production cho asset có hash trong filename:
Cache-Control: public, max-age=31536000, immutablequaSetResponseHeaderLayerwrapServeDir. ETag tự động set bởiServeDircho conditional GET 304 Not Modified. - Shop API CDN strategy lock B27: KHÔNG tự serve product image / JS bundle / CSS — đẩy lên S3 hoặc Cloudflare R2 + serve qua CDN (CloudFront, Cloudflare, Bunny). Exception duy nhất là Swagger UI bundled qua
utoipa-swagger-ui(lock B8). - Pattern redirect 307 từ REST endpoint tới CDN URL khi cần shortcut (vd
GET /products/:slug/imageredirect 307 tớihttps://cdn.shop.com/products/<slug>.webp) — 307 vì URL CDN có thể đổi khi migrate provider + preserve method nếu client dùng HEAD.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Phân biệt 301 vs 308: cái nào permanent? cái nào preserve method? Khi nào nên chọn 308 thay vì 301?
- POST
/loginthành công nên redirect đến/dashboardvới status code gì? Vì sao không dùng 302 hoặc 307? tower-httpServeDirvàServeFilecần feature gì trongCargo.toml? Mount qua method nào củaRouter— khác gì vớinestthông thường?- Shop API serve product image qua CDN hay tự host? Tại sao chọn vậy? Exception nào được tự serve static từ app server?
- Asset có hash trong filename (
app.abc123.js) nên setCache-Controlgì? Vì sao thêm flagimmutable? Combo vớiETagwork như thế nào?
Đáp án
- 301 vs 308: cả hai đều permanent (client cache vĩnh viễn). Khác biệt cốt lõi ở method preserve: 301 cho phép client đổi method (lịch sử HTTP/1.0 nhiều browser tự convert POST → GET sau 301 dù RFC khuyến nghị giữ), 308 BẮT BUỘC preserve method + body. RFC 9110 bổ sung 308 vào năm 2015 (RFC 7538) để khắc phục ambiguity của 301. Khi nào chọn 308 thay 301: (a) endpoint có thể nhận POST/PUT/PATCH (API endpoint migration
/api/v0/products→/api/v1/productscần giữ method + body); (b) muốn dễ rollback hơn — 308 cache yếu hơn 301 ở một số browser implementation, sai có thể fix nhanh hơn. axum 0.8Redirect::permanenttrả 308 (KHÔNG phải 301) — phù hợp default modern. Để emit 301 raw bạn tự build tuple(StatusCode::MOVED_PERMANENTLY, [(header::LOCATION, "/new")]); chỉ dùng khi rename URL vĩnh viễn cho GET-only resource (SEO benefit search engine update index, tận dụng cache 301 cũ hơn). - POST
/loginthành công redirect đến/dashboardnên dùng 303 See Other (quaRedirect::to("/dashboard")). Vì sao: (a) 303 luôn force GET cho request tiếp theo bất kể method gốc — đúng intent vì/dashboardlà GET endpoint render HTML, không nhận POST; (b) tránh issue user refresh trang dashboard browser resubmit POST/login(kinh điển "Confirm Form Resubmission" dialog Chrome — phá UX). KHÔNG dùng 302 vì semantic mơ hồ — RFC 9110 khuyến nghị 303 hoặc 307 thay thế, 302 chỉ giữ cho backward compat legacy. KHÔNG dùng 307 vì 307 preserve method — browser tiếp tục gửi POST/dashboardvới body username/password → endpoint dashboard nhận POST không expect → fail hoặc trả 405 Method Not Allowed. Pattern POST-redirect-GET là use case kinh điển của 303, lock cho admin form submit Shop API (B105). - Feature cần thêm:
fsvàotower-httptrongCargo.tomlworkspace dependencies. Shop API lock B10 hiện có 3 feature (trace+cors+compression-gzip), khi cần serve static thực tế add thêmfs— vd:tower-http = { version = "0.6", features = ["trace", "cors", "compression-gzip", "fs"] }. Mount method:ServeDirquaRouter::nest_service("/static", ServeDir::new("public"))vàServeFilequaRouter::route_service("/favicon.ico", ServeFile::new("assets/favicon.ico")). Khácnest/routethông thường:nestnhậnRoutercon (đăng ký nhiều route qua axum API qua các.route()chain),nest_servicenhậntower::Servicetrực tiếp (single service xử lý mọi request đến prefix);ServeDirvàServeFilelàtower::Servicechứ không phảiaxum::handler::Handlernên BẮT BUỘC dùng phiên bản_service. Tương tựroutevsroute_servicecho single path. - Shop API serve product image qua CDN (S3 / Cloudflare R2 + CloudFront / Cloudflare CDN / Bunny CDN), KHÔNG tự host từ app server. 4 lý do: (a) App server tập trung business logic — code Rust optimize cho concurrency + low-latency DB access, không nên cạnh tranh resource serve image GB; (b) Edge caching globally — CDN có POP ở hàng chục city, user Hà Nội fetch image từ POP Singapore latency ~30ms thay vì cross-Pacific tới app server US ~200ms; (c) Auto scale, không tốn bandwidth backend — Black Friday spike image fetch 100x CDN absorb hoàn toàn, app server không thấy load này; (d) Cost — bandwidth CDN ($0.01-0.04/GB) rẻ hơn nhiều bandwidth từ datacenter compute instance ($0.09/GB AWS EC2 egress). Exception duy nhất: Swagger UI static (HTML/CSS/JS bundled qua
utoipa-swagger-uicrate lock B8) — bundle ~500KB embed vào binary quainclude_bytes!macro, serve trực tiếp từ memory, lý do không upload CDN vì cần tích hợp chặt với OpenAPI spec runtime (spec generate code-first từ utoipa derive macro). Upload flow: B247 chi tiết presigned URL — client upload trực tiếp từ browser lên S3, app server chỉ generate signature thời hạn 5 phút và lưu meta data DB, không proxy bytes. - Asset có hash trong filename (
app.abc123.js) nên setCache-Control: public, max-age=31536000, immutable. Giải nghĩa từng directive: (a)public— cho phép cả browser cache lẫn intermediate proxy/CDN cache (khácprivatechỉ cache browser); (b)max-age=31536000— 1 năm (60×60×24×365 giây), thời gian browser giữ response trong local cache không hỏi lại server; (c)immutable— flag báo browser không re-validate dù user nhấn refresh (Ctrl+R / Cmd+R), tận dụng tối đa cache, chỉ revalidate khi hard refresh (Ctrl+Shift+R). Vì sao thêm immutable: hash trong filename đảm bảo content không bao giờ đổi (URLapp.abc123.jsluôn map cùng bytes), build mới sinh hash khác → filename khác → URL khác → cache busting tự nhiên qua filename; flagimmutablebáo browser skip cả conditional GET (If-None-Match) tiết kiệm 1 round-trip. Combo với ETag:ServeDirsetETagtự động dựa file mtime + size, browser conditional GET vớiIf-None-Match: "abc123"trong request tiếp theo, server compare ETag match → trả 304 Not Modified body rỗng (tiết kiệm bandwidth, browser dùng cached body). Patternmax-agedài + ETag conditional GET tối ưu cho static asset production — first hit full download, subsequent hit trong 1 năm bypass server hoàn toàn (immutable) hoặc 304 nhỏ (không immutable).
Bài Tiếp Theo
Bài 28: Route Với State<T> — chi tiết Router::with_state(state) provide AppState cho mọi handler, State<AppState> extractor trong signature handler, AppState chứa pool + redis + config (preview G6/G18), pattern share state cross-handler, testing với mock state.
