Mục lục
- Mục Tiêu Bài Học
- 3 Algorithm Compression Phổ Biến
- HTTP Content Negotiation: Accept-Encoding + Content-Encoding
tower-http::CompressionLayerResponse Compressioncompress_whenPredicate — Khi Nào Compresstower-http::DecompressionLayerRequest Body- Benchmark Thực Tế: gzip Vs brotli
- Pitfall + Streaming Compression
- Tổng Kết Group 5 + Roadmap Group 6
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu 3 algorithm phổ biến: gzip, deflate, brotli — pros/cons mỗi loại.
- Hiểu cơ chế HTTP content negotiation qua header
Accept-Encoding+Content-Encoding. - Áp dụng
tower-http::CompressionLayercho response (server → client). - Áp dụng
tower-http::DecompressionLayercho request body (client → server). - Benchmark thực tế gzip vs brotli trên response JSON Shop API.
- Hiểu pitfall: content-length mismatch, double compression, streaming + compression, CRIME attack.
- Hoàn thành Group 5 JSON Body Streaming, sẵn sàng sang Group 6 PostgreSQL + sqlx.
3 Algorithm Compression Phổ Biến
HTTP Compression giảm bandwidth bằng cách encode response trước khi gửi qua mạng. Ba algorithm phổ biến trong REST API hiện đại:
- gzip (RFC 1952): phổ biến nhất, support đầy đủ mọi browser + CDN + HTTP client legacy. Tốc độ encode/decode cân bằng. Ratio compress JSON/HTML/JavaScript text ~70% (file 100KB còn ~30KB). Default choice cho mọi server HTTP từ 2000s.
- deflate (RFC 1951): chính là underlying algorithm của gzip —
gzip = deflate + header CRC32. Implementation buggy ở một số HTTP client cũ (Internet Explorer 6, một số proxy 2005-2010), nên industry recommendation skip deflate standalone, luôn dùng gzip. - brotli (br, RFC 7932): Google develop 2015, ratio tốt hơn gzip ~15-25% trên text content. Browser modern support (Chrome 50+, Firefox 44+, Safari 11+). Đặc điểm: slower compress (server pay CPU cost) nhưng faster decompress (client save CPU + bandwidth). Recommendation: bật cho static asset CDN (level 11) + JSON response dynamic (level 4).
Bảng so sánh nhanh:
Algorithm | Speed encode | Speed decode | Ratio JSON | Support
gzip | Fast | Fast | ~70% | Universal
deflate | Fast | Fast | ~70% | Old client buggy
brotli | Medium | Fast | ~55-60% | Modern only
Decision lock Shop API: bật cả gzip + brotli, client chọn algorithm qua header Accept-Encoding. Skip deflate (tránh edge case legacy client) và skip zstd (Facebook 2016, support browser còn hẹp 2026, để dành G19 nếu cần ratio tốt hơn brotli).
HTTP Content Negotiation: Accept-Encoding + Content-Encoding
Client báo cho server biết các algorithm nó hỗ trợ qua header Accept-Encoding, server chọn algorithm phù hợp và báo qua Content-Encoding trong response. Đây là cơ chế content negotiation (lock B5 đã cover dimension media-type, B50 cover dimension encoding).
Request:
GET /api/v1/products HTTP/1.1
Accept-Encoding: gzip, br, deflate
Response:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Encoding: br
Vary: Accept-Encoding
Transfer-Encoding: chunked
<brotli-compressed body>
Client có thể ưu tiên algorithm qua quality value q=:
Accept-Encoding: br;q=1.0, gzip;q=0.8, *;q=0.1
Server đọc q-value, sắp xếp từ cao xuống thấp, chọn algorithm đầu tiên mà server hỗ trợ. Pattern lock: response server MUST include header Content-Encoding mỗi khi body được compress, thiếu thì client decode sai.
Header Vary: Accept-Encoding báo cho CDN + proxy cache rằng response phụ thuộc vào client capability — mỗi giá trị Accept-Encoding khác nhau cần variant cache riêng. Thiếu Vary thì client A nhận response brotli được cache lại, client B không support brotli vẫn nhận được response brotli → decode fail.
tower-http tự handle toàn bộ flow này: parse Accept-Encoding, chọn algorithm, set Content-Encoding + Vary trong response. Bạn không phải code thủ công.
tower-http::CompressionLayer Response Compression
Bước 1 — extend feature flag cho tower-http trong workspace root Cargo.toml. Workspace lock B10 đang dùng feature compression-gzip minimal, B50 nâng lên compression-full bao trùm cả 4 algorithm:
# File: shop/Cargo.toml (workspace root, updated B50)
[workspace.dependencies]
tower-http = { version = "0.6", features = [
"trace",
"cors",
"compression-full", # B50: enable gzip + br + deflate + zstd
"decompression-full", # B50: enable decompress request body
] }
Feature compression-full kích hoạt cả gzip + brotli + deflate + zstd thay vì khai báo từng feature riêng. Tương tự decompression-full cho 4 algorithm decompress request body.
Bước 2 — wire vào router.rs. File này đã có 2 middleware từ B39 (request_id_middleware + enrich_error_response), B50 thêm CompressionLayer ở vị trí OUTERMOST trong stack:
// File: crates/shop-api/src/router.rs (extend B50)
use axum::{middleware, routing::get, Router};
use tower_http::compression::{
predicate::{And, NotForContentType, SizeAbove},
CompressionLayer,
};
use tower_http::decompression::DecompressionLayer;
use crate::{handlers, middleware as mw, routes, state::AppState};
pub fn build_router(state: AppState) -> Router {
let api_v1 = Router::new()
.merge(routes::products::routes());
// B50: compression cho response server → client
let compression = CompressionLayer::new()
.gzip(true)
.br(true)
.deflate(false) // skip deflate (lock B50)
.zstd(false) // skip zstd compat (lock B50)
.quality(tower_http::CompressionLevel::Default)
.compress_when(
SizeAbove::new(512)
.and(NotForContentType::IMAGES)
.and(NotForContentType::const_new("application/zip"))
.and(NotForContentType::const_new("application/gzip")),
);
// B50: decompression cho request body client → server
let decompression = DecompressionLayer::new()
.gzip(true)
.br(true)
.deflate(false);
Router::new()
.route("/", get(root))
.merge(routes::health::routes())
.merge(routes::version::routes())
.nest("/api/v1", api_v1)
.fallback(handlers::fallback::not_found)
.method_not_allowed_fallback(handlers::fallback::method_not_allowed)
// Layer stack bottom-up apply (B29 lock + B39 lock continued):
.layer(middleware::from_fn(mw::enrich_error_response)) // INNER
.layer(middleware::from_fn(mw::request_id_middleware)) // middle
.layer(decompression) // request decode
.layer(compression) // OUTERMOST
.with_state(state)
}
Middleware ordering lock B39 bottom-up continued: axum apply layer từ dưới lên (cuối khai báo = ngoài cùng runtime). Stack runtime của Shop API sau B50:
[Client]
│
▼
1. compression (OUTERMOST — compress response cuối cùng)
│
▼
2. decompression (decompress request body sớm)
│
▼
3. request_id (B39 — generate/inject X-Request-Id)
│
▼
4. enrich_error (B39 INNER — inject request_id vào error body)
│
▼
5. handler (business logic)
Lý do CompressionLayer đặt OUTERMOST: compression chỉ thao tác trên bytes response cuối cùng sau khi handler + middleware INNER đã build xong body. Đặt ở INNER thì các layer ngoài (request_id) sẽ thao tác trên bytes đã compress → sai logic enrich.
compress_when Predicate — Khi Nào Compress
tower-http không compress mọi response — predicate compress_when quyết định response nào đáng compress. Shop API compose 4 predicate qua .and() operator:
// Snippet predicate trong build_router (extract từ step 4)
.compress_when(
SizeAbove::new(512)
.and(NotForContentType::IMAGES)
.and(NotForContentType::const_new("application/zip"))
.and(NotForContentType::const_new("application/gzip")),
)
SizeAbove::new(512): chỉ compress response lớn hơn 512 bytes. Lý do MTU — packet TCP chuẩn Ethernet ~1500 bytes, response nhỏ hơn 1 packet không cần compress vì:
- Network truyền 1 packet hay 1 packet đều cùng 1 round-trip.
- Compress nhỏ tốn CPU mà không giảm số packet.
- Overhead header compression (Content-Encoding, Vary) chiếm phần lớn payload.
Threshold 512 bytes là default lock Shop API — cân bằng giữa CPU cost (skip nhỏ) và bandwidth saving (catch response trung bình 1-10KB). Tunable qua benchmark nếu cần.
NotForContentType::IMAGES: skip PNG/JPEG/WebP/GIF — các format này đã compress trong format (DEFLATE bên trong PNG, DCT bên trong JPEG). Compress lần 2 không giảm size đáng kể, còn tốn CPU.
Custom skip cho application/zip + application/gzip (file đã compress) và video/audio (compress bằng codec H.264/AAC riêng):
Content type | Compress?
application/json | YES (text repeat)
application/x-ndjson | YES (line repeat)
text/html | YES
text/css | YES
image/png | NO (PNG đã DEFLATE bên trong)
image/jpeg | NO (JPEG đã DCT compress)
application/zip | NO (đã compress)
application/gzip | NO (đã compress)
video/mp4 | NO (H.264 codec)
Pattern lock Shop API B50: compress JSON (application/json + application/x-ndjson) — skip binary/compressed content type. Hợp với decision matrix endpoint B49 (paginated list JSON + NDJSON export/import đều text).
tower-http::DecompressionLayer Request Body
Compression hai chiều — không chỉ server compress response, client cũng có thể compress request body. Use case chính của Shop API: bulk import NDJSON 100MB nén còn 10MB upload nhanh hơn + tiết kiệm bandwidth client mobile.
# Client gửi NDJSON đã gzip
gzip -c products.ndjson > products.ndjson.gz
curl -X POST http://localhost:3000/api/v1/products/import.ndjson \
-H 'Content-Type: application/x-ndjson' \
-H 'Content-Encoding: gzip' \
--data-binary @products.ndjson.gz
DecompressionLayer đọc header Content-Encoding: gzip, decompress body stream trước khi handler đọc:
// File: crates/shop-api/src/router.rs (snippet B50 decompression)
use tower_http::decompression::DecompressionLayer;
let decompression = DecompressionLayer::new()
.gzip(true)
.br(true)
.deflate(false); // align với compression layer skip deflate
Pitfall body limit: lock B47 đã set DefaultBodyLimit::max(10 * 1024 * 1024) per-route 10MB cho import endpoint NDJSON. Khi bật decompression, thứ tự apply là:
Client → 10MB compressed body
│
▼
DefaultBodyLimit check 10MB (PASS — body raw 10MB)
│
▼
DecompressionLayer decompress → 100MB+ NDJSON text
│
▼
Handler đọc body 100MB+ (KHÔNG cap)
Bài toán: client gửi 10MB compressed (qua limit) decompress thành 100MB+ → memory exhaustion. Solution Shop API B50:
- Giữ
DefaultBodyLimit::max(10MB)trên compressed input (lock B47 continued). - Thêm custom cap 100MB sau decompress qua
tower::util::option_layervớiRequestBodyLimitLayer::new(100 * 1024 * 1024)đặt INNER hơnDecompressionLayer. - Hoặc đơn giản hơn: trong handler import_products_ndjson, đếm bytes processed, abort nếu vượt 100MB (B49 logic đã có line counter).
Lock decision Shop API: bật DecompressionLayer cho mọi request + giữ 10MB compressed cap + thêm 100MB decompressed cap trong handler logic (preview G6 khi DB write cần stream insert).
Benchmark Thực Tế: gzip Vs brotli
Test thực tế trên endpoint GET /api/v1/products trả 1000 product JSON (~500KB raw). Server tự chọn algorithm theo Accept-Encoding:
cargo run --release -p shop-api
# Test gzip
curl -H 'Accept-Encoding: gzip' \
--compressed \
-w 'size_download: %{size_download}\nsize_request: %{size_request}\n' \
-o /dev/null \
http://localhost:3000/api/v1/products
# Test brotli
curl -H 'Accept-Encoding: br' \
--compressed \
-w 'size_download: %{size_download}\n' \
-o /dev/null \
http://localhost:3000/api/v1/products
# Test không compress
curl -H 'Accept-Encoding: identity' \
-w 'size_download: %{size_download}\n' \
-o /dev/null \
http://localhost:3000/api/v1/products
Kết quả tham khảo (numbers minh họa workload Shop API):
Algorithm | Original | Compressed | Ratio | Encode time | Decode time
identity | 500KB | 500KB | 100% | 0ms | 0ms
gzip(6) | 500KB | 70KB | 14% | 8ms | 2ms
gzip(9) | 500KB | 65KB | 13% | 18ms | 2ms
brotli(4) | 500KB | 60KB | 12% | 12ms | 3ms
brotli(11)| 500KB | 50KB | 10% | 200ms | 3ms
Observations:
- gzip(6) → 14% ratio, encode 8ms: default sweet spot cho response dynamic.
- gzip(9) → 13% ratio, encode 18ms: tốn gấp đôi CPU cho 1% saving — không đáng.
- brotli(4) → 12% ratio, encode 12ms: tốt hơn gzip ~2% ratio với 50% CPU thêm — đáng cho client modern.
- brotli(11) → 10% ratio, encode 200ms: chỉ dùng cho static asset CDN (pre-compress 1 lần, serve nhiều lần), KHÔNG dùng cho dynamic response.
Lock Shop API B50: gzip(6) + brotli(4) — balance speed vs ratio. tower-http::CompressionLevel::Default map sang level này cho cả 2 algorithm.
Default level của tower-http 0.6 hiện tại lock workspace B10:
// Default level (lock Shop API B50)
CompressionLayer::new()
.gzip(true)
.br(true)
.quality(tower_http::CompressionLevel::Default) // gzip 6, br 4
Cá nhân hóa level qua CompressionLevel::Precise(N) nếu cần benchmark cụ thể workload — không khuyến nghị tùy ý chỉnh.
Pitfall + Streaming Compression
Pitfall 1 — Content-Length mismatch:
Response uncompressed có Content-Length: 500000, sau compress còn 70000 bytes nhưng header cũ vẫn 500000 → client decode sai. Solution: tower-http tự strip Content-Length, dùng Transfer-Encoding: chunked stream từng chunk có header length riêng. Bạn không phải code thủ công.
curl -I http://localhost:3000/api/v1/products \
-H 'Accept-Encoding: gzip'
# HTTP/1.1 200 OK
# content-type: application/json; charset=utf-8
# content-encoding: gzip
# vary: accept-encoding
# transfer-encoding: chunked ← thay vì content-length
# x-request-id: 550e8400-...
Pitfall 2 — Double compression:
CDN (CloudFlare, Vercel, AWS CloudFront) thường tự compress response trước khi gửi client. Server compress nữa → response có Content-Encoding: gzip, gzip hoặc CDN không re-compress được do đã thấy Content-Encoding → client decode 1 lần, vẫn còn 1 lớp gzip → JSON parse fail.
Solution: check header CF-Cache-Status (CloudFlare) hoặc X-Forwarded-Encoding trong middleware để skip compression nếu CDN handle. Hoặc đơn giản nhất: disable server compression ở production khi đứng sau CDN, cho CDN handle entirely (decision G19 production deploy).
Pitfall 3 — Streaming + compression (quan trọng cho B49 NDJSON):
NDJSON export Shop API dùng Body::from_stream (lock B38 + B49 continued) — body chunked, mỗi chunk 1 product line. CompressionLayer compress từng chunk độc lập:
Stream uncompressed:
{"id":1,"name":"iPhone 15",...}\n ← chunk 1
{"id":2,"name":"Samsung S24",...}\n ← chunk 2
Stream compressed:
<gzip-chunk-1> ← compress chunk 1
<gzip-chunk-2> ← compress chunk 2
...
Pitfall: chunk nhỏ ~100 bytes thì compress overhead (gzip header per chunk) làm output có khi LỚN hơn input. tower-http auto-flush mỗi 8KB chunk default — buffer 80 line NDJSON rồi compress 1 lần, ratio tốt hơn từng line.
Kết hợp NDJSON export + compression: bandwidth giảm 5-10 lần cho dataset structure repeat (log, product catalog). Verify:
curl --compressed \
-w 'size_download: %{size_download}\n' \
-o products.ndjson \
http://localhost:3000/api/v1/products/export.ndjson
# size_download: 80000 (raw 500KB → gzip ~80KB)
# wc -l products.ndjson → 1000 line
Pitfall 4 — CRIME attack:
CRIME (2012) lợi dụng compression size leak token sensitive — attacker gửi nhiều request với payload đoán dần, đo size response compressed để guess giá trị token CSRF/session. Mitigation:
- Dùng
Vary: Accept-Encoding+ KHÔNG compress response chứa CSRF token / session cookie. - Shop API stateless với JWT bearer (lock B11) — KHÔNG có session cookie trong response body. Token chỉ ở header
Authorizationrequest (client gửi), response không echo lại. tower-httpdefault an toàn cho REST API stateless — không có pattern leak token qua response size.
Lock B50 mitigation: stateless JWT bearer + KHÔNG compress error response chứa request_id sensitive (B39 enrich error đã set Content-Type: application/json, compression apply OK vì envelope không leak credential).
Tổng Kết Group 5 + Roadmap Group 6
Group 5 JSON Body Streaming đã cover 10 bài (B41-B50):
- B41: JSON extract + validation với
validatorcrate (ValidatedJson extractor). - B42: Optional field pitfalls + double-Option pattern PATCH (RFC 7396).
- B43: Enum tagged 4 cách + Shop API internally tagged cho PaymentMethod.
- B44: Newtype + transparent + Money(Decimal) lock JSON string format.
- B45: Skip + rename + security pattern Entity vs Response DTO tách biệt.
- B46: Custom serializer + Visitor + Phone newtype VN normalize.
- B47: Collection serde + DoS defense 4 layer (Vec/BTreeSet/BTreeMap).
- B48: JSON error path detail +
serde_path_to_errormandatory. - B49: NDJSON streaming + ImportReport envelope resilient batch.
- B50: HTTP Compression gzip + brotli + middleware ordering OUTERMOST.
Foundation đã ready cho Group 6+:
- DTO layer hoàn chỉnh:
CreateProductDto+UpdateProductDto+ProductResponseDto+ProductListResponse+UserResponseDto+PaymentMethod. - Type safety:
Money(Decimal)+Phone+ 5 ID newtype (UserId/ProductId/OrderId/CategoryId). - Error envelope: 14
AppErrorvariant +serde_path_to_errorpath detail. - Bulk endpoint: NDJSON export/import pattern + ImportReport.
- Wire transport: gzip + brotli auto compression Shop API toàn cục.
Group 6 PostgreSQL + sqlx (B51-B60) sẽ cover:
- PostgreSQL 17 setup + Docker compose dev.
- sqlx-cli + migration system (sqlx-migrate).
- Connection pool
PgPoolOptions+ tuning. - Query macros
sqlx::query!vs query function trade-off. - Type mapping Rust ↔ Postgres (chrono, uuid, Decimal, JSONB).
- Transaction + savepoint nested.
- Benchmark connection pool size.
- Tablespace + partition cơ bản preview.
Sau Group 6, Shop API có database thật — refactor ProductService từ in-memory mock sang sqlx pool, NDJSON export B49 thay placeholder bằng sqlx::query_as!(...).fetch(&pool) stream native.
Tổng Kết
- 3 algorithm: gzip (universal, ratio 70%), deflate (skip do bug client cũ), brotli (modern, better ratio ~55-60%).
- Accept-Encoding + Content-Encoding content negotiation client + server thỏa thuận algorithm.
Vary: Accept-EncodingCDN cache variant theo client capability (tower-http tự set).CompressionLayerShop API: gzip + br, threshold 512 bytes MTU-aligned, skip binary content type (images/zip/gzip/video/audio).DecompressionLayercho request body — pitfall body limit B47 áp trước decompress, thêm custom 100MB cap sau decompress.- Middleware ordering:
compressionOUTERMOST >decompression>request_id>enrich_errorINNER (lock B39 bottom-up continued). - Compression level lock:
gzip(6) + brotli(4)— balance speed/ratio.brotli(11)chỉ dùng static CDN. - 4 pitfall: Content-Length mismatch (auto-fix Transfer-Encoding chunked), double compression (CDN check), streaming flush 8KB (NDJSON kết hợp tốt), CRIME attack (REST stateless JWT bearer an toàn).
- HOÀN THÀNH Group 5 (10/10 bài) — foundation DTO + type safety + error envelope + bulk endpoint + wire transport ready cho G6 PostgreSQL.
- Workspace deps:
tower-httpthêm featurecompression-full+decompression-full. - File path lock: extend
crates/shop-api/src/router.rswire 2 layer compression + decompression.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 3 algorithm gzip/deflate/brotli khác nhau ra sao? Tại sao Shop API skip deflate?
Accept-Encodingquality valueq=dùng để làm gì? Cho ví dụ negotiation thực tế.SizeAbove::new(512)predicate: tại sao threshold 512 bytes? Lý do MTU và CPU trade-off.- Decompression layer + body limit: limit áp trước hay sau decompress? Pitfall và solution.
- Middleware ordering: tại sao compression đặt OUTERMOST trong stack? So với request_id và enrich_error.
Đáp án
- 3 algorithm khác nhau: (a) gzip RFC 1952 ra đời 1992, underlying là DEFLATE + GZIP header chứa CRC32 + ISIZE; universal support mọi HTTP client từ 1.1+ (1997); ratio JSON ~70% (file 100KB còn 30KB); encode/decode đều nhanh ~vài ms cho 500KB; sweet spot mọi REST API. (b) deflate RFC 1951 chính là underlying algorithm bên trong gzip — chỉ thiếu header CRC32. Lý thuyết hợp lý dùng standalone tiết kiệm vài byte header. Thực tế HTTP client cũ (Internet Explorer 6, một số proxy 2005-2010) buggy parse khác nhau, có client expect raw DEFLATE, client khác expect zlib-wrapped DEFLATE → response không decode được. Industry recommendation từ 2010: skip deflate standalone, luôn dùng gzip cho safety. (c) brotli RFC 7932 Google develop 2015, dictionary preset 119KB context tiếng Anh + JavaScript + HTML giúp ratio tốt hơn gzip ~15-25% trên text web. Slower encode 1.5-2x gzip ở level tương đương, faster decode tương đương. Support Chrome 50+ (2016), Firefox 44+ (2016), Safari 11+ (2017), Edge 15+. Tại sao Shop API skip deflate: (i) trùng underlying với gzip không thêm value mới; (ii) edge case legacy client buggy parse → support effort không đáng cho gain 0%; (iii) modern HTTP client 2026 luôn support gzip nên fallback từ brotli xuống gzip là enough; (iv)
tower-httpfeaturecompression-fulldefault enable deflate nhưng config.deflate(false)tắt rõ ràng trong coderouter.rsđể dev sau hiểu intent. Lock decision Shop API B50: gzip + brotli ON, deflate + zstd OFF, deflate có thể bật lại tương lai nếu telemetry cho thấy >0.1% client requestAccept-Encoding: deflatestandalone (chưa từng thấy production data). - Quality value
q=trongAccept-Encodinglà cơ chế client báo cho server thứ tự ưu tiên các algorithm nó hỗ trợ. Giá trị từ 0.0 (không muốn) đến 1.0 (ưu tiên nhất), default 1.0 nếu không khai báo. Server đọc q-value, sắp xếp algorithm từ cao xuống thấp, chọn algorithm đầu tiên mà server có hỗ trợ. Ví dụ negotiation thực tế: (a)Accept-Encoding: br;q=1.0, gzip;q=0.8, *;q=0.1— client (Chrome 100) ưu tiên brotli, fallback gzip, fallback wildcard cho mọi algorithm khác với q=0.1; server Shop API config.gzip(true).br(true).deflate(false)sẽ chọn brotli (q=1.0 match được); response trảContent-Encoding: br. (b)Accept-Encoding: gzip, deflate— client (HTTP client legacy không support brotli) khai báo 2 algorithm, q default 1.0 cả hai; server chọn algorithm đầu tiên match: gzip OK (Shop API support), trảContent-Encoding: gzip. (c)Accept-Encoding: br;q=1.0, identity;q=0.5— client ưu tiên brotli, fallback uncompressed; nếu server không support brotli (Shop API có support) thì trả identity (raw, no Content-Encoding); cách dùngidentityhiếm thực tế, thường chỉ test/debug. (d)Accept-Encoding: identity;q=0, *;q=1— client KHÔNG muốn identity (raw), buộc server phải compress; nếu server không support compression nào → 406 Not Acceptable theo RFC 9110; thực tế Shop API tower-http sẽ fallback identity (im lặng OK) — không nghiêm khắc 406 vì q=0 hiếm gặp. Pattern lock Shop API B50: tower-http tự handle q-value parsing + algorithm selection, dev không phải code thủ công. Chỉ cần config.gzip(true).br(true)đủ. - Threshold
SizeAbove::new(512)bytes: con số này align với MTU (Maximum Transmission Unit) mạng Ethernet chuẩn 1500 bytes, sau khi trừ overhead TCP/IP header ~40 bytes + TLS header ~30 bytes → payload usable ~1430 bytes. Threshold 512 đặt ở khoảng nửa MTU vì lý do: (a) Response nhỏ hơn 1 packet không cần compress: gửi 1 packet hay nửa packet cùng 1 round-trip TCP, compress nhỏ tốn CPU mà không giảm số packet. (b) Overhead compression header:Content-Encoding+Vary+Transfer-Encodingtốn ~80 bytes header response — response 200 bytes compress còn 100 bytes nhưng thêm 80 bytes header → net 180 bytes vs raw 200 bytes (chỉ tiết kiệm 20 bytes, không đáng CPU). (c) Empty content threshold: response 204 No Content (DELETE), 304 Not Modified, error 401/403 chỉ vài chục bytes — compress vô nghĩa. (d) CPU cost: gzip encode 100 bytes mất ~10μs, encode 500 bytes mất ~30μs — overhead 0.1-0.3% CPU/request không đáng lo nhưng tích lại với 10K req/s = 1-3% CPU tổng. Trade-off: threshold quá cao (1500 bytes = 1 MTU) → bỏ lỡ response 600-1500 bytes có thể compress 50% còn 300-750 bytes tiết kiệm 1 packet. Threshold quá thấp (0 bytes) → compress mọi response tốn CPU mà không tiết kiệm bandwidth cho response nhỏ. 512 bytes là sweet spot được community community-recommended (nginx default, Apache mod_deflate default cũng quanh 1KB). Lock Shop API B50:SizeAbove::new(512)vĩnh viễn, tunable qua telemetry nếu workload đặc thù (vd microservice trả mostly 100-500 byte response → threshold 256, bulk endpoint mostly trả >10KB → threshold 1024 không thay đổi compression rate). - DecompressionLayer + body limit ordering: pitfall quan trọng, hay miss khi production deploy. (a) Thứ tự apply:
DefaultBodyLimit(lock B47) áp trên compressed bytes raw vào trước khi DecompressionLayer xử lý — vìDefaultBodyLimitđứng INNER hơn (gần handler) còn DecompressionLayer wrap body stream và pass-through bytes raw lúc đầu chain. Stream flow:Client → 10MB gzip body → DecompressionLayer wrap (chưa đọc) → DefaultBodyLimit check 10MB (PASS) → handler đọc body stream → DecompressionLayer decompress on-the-fly → handler nhận 100MB+ NDJSON text. (b) Pitfall: attacker gửi 10MB gzip bomb (file zip nhỏ extract thành 100GB rác) — quaDefaultBodyLimit::max(10MB)vì compressed size là 10MB; sau decompress handler đọc 100GB → memory exhaustion → DoS server. (c) Solution Shop API B50: 3 layer phòng thủ: (i) giữDefaultBodyLimit::max(10MB)per-route lock B47 (chống DoS compressed body lớn); (ii) thêmRequestBodyLimitLayer::new(100 * 1024 * 1024)đặt INNER hơnDecompressionLayertrong stack — limit này check trên bytes decompressed, abort khi vượt 100MB; (iii) trong handlerimport_products_ndjsonB49, đếm line counter + bytes processed counter, abort sớm khi line counter > 100K hoặc bytes > 100MB (defensive in-app). (d) Ratio limit: Shop API lock 10MB compressed → 100MB decompressed (ratio 1:10). Ratio cao hơn 1:10 hiếm khi NDJSON normal (text repeat ~14%), nếu vượt là dấu hiệu gzip bomb attack — abort + log security alert. (e) Production preview G19: thêm middleware custom check ratio decompressed/compressed runtime, alert khi ratio > 20 (threshold sensitive). Lock B50: 3 layer defense in-app, không trust tower-http default cho production. - Compression OUTERMOST trong stack — lý do kỹ thuật: middleware stack axum apply theo nguyên tắc bottom-up khai báo = ngoài cùng runtime (lock B29 + B39 continued). Stack B50 từ ngoài vào trong: compression → decompression → request_id → enrich_error → handler. Tại sao compression OUTERMOST: (a) Compression thao tác trên bytes response cuối cùng: chỉ sau khi handler + mọi middleware INNER đã build xong body bytes thì compression mới encode. Nếu đặt INNER thì layer ngoài (request_id, enrich_error) sẽ thao tác trên bytes ĐÃ compress → header inject sai, body decompress lỗi. (b) Enrich-error inject
request_idvào body JSON (lock B39): nếu compression đặt INNER hơn enrich_error thì enrich_error nhận body đã compressed dưới dạng bytes opaque, không parse được JSON để inject field. Phải decompress, inject, recompress — overhead cao + sai semantic. (c) Request_id middleware cần đọc/set header: nếu compression OUTER hơn request_id, response headerX-Request-Idđược set TRƯỚC khi compression encode body → header set bình thường, body compress bình thường. Nếu ngược lại request_id OUTER hơn compression → middleware đọc body đã compressed để inject request_id (sai layer abstraction). (d) Decompression cũng OUTER hơn request_id: decompress request body sớm cho handler đọc body raw — nếu đặt INNER thì handler nhận body compressed phải tự decompress (mất abstraction). (e) Quy tắc chung middleware ordering: layer thao tác trên wire format (compression, decompression, TLS termination ở reverse proxy) đặt OUTERMOST; layer thao tác trên logical request/response (request_id, auth, rate-limit, tracing) đặt middle; layer thao tác trên body content (enrich_error inject field, response envelope) đặt INNER nhất. Stack runtime Shop API B50 lock vĩnh viễn: compression (outer wire) → decompression (outer wire) → request_id (logical request) → enrich_error (inner body) → handler. Future middleware G7+ áp dụng pattern này: auth check inject sau request_id, rate-limit inject sau auth, telemetry trace inject ngoài cùng (sẽ refactor compression vào OUTER hơn telemetry ở G15).
Bài Tiếp Theo
Bài 51: PostgreSQL Setup Với sqlx — mở Group 6 PostgreSQL + sqlx: cài đặt PostgreSQL 17 local, sqlx-cli, connection string, basic query qua sqlx::query!, refactor product_service Shop API từ in-memory mock sang DB thật, foundation cho 60+ endpoint CRUD từ G7 onward.
