Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu serde behavior cho
Vec<T>,HashMap<K,V>,BTreeMap<K,V>,HashSet<T>,BTreeSet<T>khi wire ra JSON. - Phân biệt khi nào dùng
VecvsHashSetvsBTreeSettheo 3 chiều: duplicate, order stable, lookup speed. - Hiểu pitfall key non-String trong Map — JSON spec RFC 8259 chỉ chấp nhận key string thuần, Newtype key cần
Display + FromStr. - Implement size limit chống DoS với 4 layer defense (RequestBodyLimit, AppJson, DefaultBodyLimit per-route, validate array length).
- Hiểu
serde_json::from_readervsfrom_slice— streaming behavior cho file lớn vs body nhỏ. - Áp dụng
ProductResponseDto+ProductListResponseShop API hoàn chỉnh — lock pattern envelope cho mọi list endpoint G7.
Vec<T> Cơ Bản
Vec<T> là collection cơ bản nhất khi map ra JSON — serialize thành JSON array với thứ tự giữ nguyên như khi insert vào vector. Pattern phổ biến nhất là wrap Vec<T> trong struct response cho list endpoint:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductListResponse {
pub items: Vec<ProductDto>,
pub total: u64,
}
Wire format JSON:
{
"items": [
{"id": 1, "name": "iPhone 15"},
{"id": 2, "name": "Galaxy S24"}
],
"total": 2
}
Deserialize từ JSON array ngược lại về Vec<T> hoạt động tự nhiên. Empty array [] deserialize thành Vec::new() length 0 — KHÔNG fail, đúng semantic "list trống" khác null "không có list".
Pitfall Vec<u8>: serde mặc định serialize Vec<u8> ra JSON array các số nguyên [1, 2, 3, ...] chứ KHÔNG phải base64 string. Nếu cần wire dạng base64 (binary data, image bytes, file checksum), bạn phải thêm #[serde(with = "serde_with::base64")] qua crate serde_with hoặc tự implement custom serializer (mức 3 lock B46). Default behavior này gây bug nếu bạn không biết — endpoint upload image trả Vec<u8> direct ra JSON sẽ là array số rất dài, không phải base64 client expect.
Pitfall Vec lớn: Vec<T> được load in-memory hết trước khi serialize ra response, hoặc khi deserialize từ JSON body. Client gửi JSON array 1 triệu phần tử sẽ allocate Vec capacity 1 triệu — RAM exhaust dễ dàng. Bước 6 sẽ cover 4 layer defense chống DoS.
Decision lock Shop API: Vec<T> dùng cho mọi list endpoint khi thứ tự quan trọng + cho phép duplicate — cart items (cùng product 2 lần khác variant), category_ids của product (admin sort theo độ ưu tiên), tags wire (sẽ cover ở bước 3 — trừ tags Shop API lock BTreeSet).
HashSet<T> Vs Vec<T>
HashSet<T> serialize ra JSON array giống Vec<T> nhưng loại duplicate tự động và order unstable mỗi lần serialize có thể đổi vị trí phần tử:
use std::collections::HashSet;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct UserPermissions {
pub user_id: UserId,
pub roles: HashSet<String>,
}
Wire format có thể là ["admin", "user"] request đầu, rồi ["user", "admin"] request sau — cùng dữ liệu nhưng JSON khác nhau byte-by-byte.
Pitfall HashSet cho wire output:
- Test snapshot fail flaky — test compare JSON output với fixture cố định, mỗi lần chạy CI có thể fail random vì order đổi.
- Audit log không reproducible — log JSON cùng user 2 request liền nhau ra 2 dòng khác nhau, code review tưởng có bug data drift.
- HTTP cache miss — ETag tính từ body hash, body thay đổi byte → ETag đổi → cache invalidate vô lý.
- JSON vào Set: deserialize JSON array
["a", "b", "a"]vàoHashSetsilent loại duplicate, length 2 không phải 3 — client gửi data sai không biết.
Giải pháp: BTreeSet<T> — Set với B-tree internal sort by key, serialize ra JSON array alphabetical stable. Cùng dữ liệu, mọi lần serialize đều ra cùng 1 chuỗi byte.
use std::collections::BTreeSet;
#[derive(Serialize, Deserialize)]
pub struct UserPermissions {
pub user_id: UserId,
pub roles: BTreeSet<String>,
}
Wire format luôn là ["admin", "user"] — alphabetical guaranteed.
Bảng decision khi nào dùng nào:
Collection | Order | Duplicate | Lookup | Use case
Vec<T> | Insert | Yes | O(n) | List cart items, ordered insert
HashSet<T> | Unstable | No | O(1) | Internal lookup tag, role check
BTreeSet<T> | Alphabet | No | O(log n) | Output wire stable, snapshot test
Shop API decision lock:
- Roles, tags wire output →
BTreeSet<String>stable order, test snapshot reliable, ETag cache hit ổn định. - Cart items, category_ids, products list →
Vec<T>preserve order add (admin sort thủ công thứ tự hiển thị). - HashSet chỉ dùng cho internal lookup nhanh trong handler/service — vd check role permission
if user_roles.contains(&"admin".to_string())O(1); KHÔNG serialize HashSet ra wire.
HashMap<K, V> Vs BTreeMap<K, V>
HashMap<K, V> serialize ra JSON object với key order unstable — mỗi lần serialize có thể đổi thứ tự field:
use std::collections::HashMap;
#[derive(Serialize)]
pub struct Metadata {
pub tags: HashMap<String, String>,
}
Wire format có thể là {"warranty": "12 months", "brand": "Apple"} request này rồi {"brand": "Apple", "warranty": "12 months"} request sau — JSON object về spec đồng nghĩa (mục đề cập tới key/value), nhưng byte stream khác nhau.
BTreeMap<K, V> giải quyết — key sort alphabetical stable nhờ B-tree internal:
use std::collections::BTreeMap;
#[derive(Serialize)]
pub struct Metadata {
pub tags: BTreeMap<String, String>,
}
Wire format luôn là {"brand": "Apple", "warranty": "12 months"} — key alphabetical guaranteed.
Shop API decision lock (B44 continued):
BTreeMap<String, Value>MANDATORY cho mọi metadata field wire output — đã lock từ B44 trênCreateProductDto.metadata. Test snapshot stable, audit log reproducible, HTTP cache ETag không drift, webhook signature verify đúng (Stripe sign body, byte order đổi → signature mismatch).HashMap<K, V>chỉ cho internal lookup nhanh trong handler/service KHÔNG serialize — vd in-memory rate limit counterHashMap<IpAddr, u32>, cache lookupHashMap<ProductId, ProductDto>warm cache 100 product hot; nếu cần wire ra response thì convert sangBTreeMaphoặcVec<{key, value}>.
Pitfall test với HashMap: dev paste JSON expected order theo Rust struct field order, viết assert_eq!(actual_json, expected_json) — fail random vì HashMap shuffle. Fix bằng cách dùng BTreeMap hoặc compare JSON parsed (assert_eq!(serde_json::from_str::<Value>(&actual), serde_json::from_str::<Value>(&expected))) thay byte-level.
Key Non-String Pitfall
JSON spec RFC 8259 mục 7 quy định key trong JSON object bắt buộc là string:
{"user_1": 100, "user_2": 200} // valid — key string
{1: 100, 2: 200} // invalid — key integer KHÔNG phải JSON
Rust HashMap cho phép key bất kỳ type nào impl Hash + Eq — kể cả số nguyên hoặc Newtype:
use std::collections::HashMap;
use serde::Serialize;
#[derive(Serialize)]
pub struct UserScores {
pub scores: HashMap<UserId, u64>,
}
serde_json giải quyết mismatch này bằng cách convert key sang string qua Display trait khi serialize, và parse string key về type gốc qua FromStr trait khi deserialize. Wire format trở thành:
{"scores": {"1": 100, "2": 200}}
Key 1 integer Rust → key "1" string JSON. Round-trip hoạt động nếu type key impl đủ 2 trait Display + FromStr.
Pitfall Newtype key chưa impl đủ trait: Newtype UserId(pub u64) B44 chỉ derive Serialize + Deserialize + Hash — chưa có Display + FromStr. Compile fail khi serialize HashMap<UserId, V> với error message khó hiểu về trait bound. Lock decision Shop API: mọi Newtype dùng làm Map key MANDATORY impl Display + FromStr — sẽ deep B58.
Alternative pattern idiomatic JSON tránh edge case key conversion: dùng Vec<{key_field, value_field}> thay Map:
#[derive(Serialize)]
pub struct UserScoreEntry {
pub user_id: UserId,
pub score: u64,
}
#[derive(Serialize)]
pub struct UserScores {
pub scores: Vec<UserScoreEntry>,
}
Wire format trở thành array of object — idiomatic JSON, không có pitfall key conversion:
{
"scores": [
{"user_id": 1, "score": 100},
{"user_id": 2, "score": 200}
]
}
Bảng decision matrix:
Use case | Pattern khuyến nghị
Lookup by key trong handler | HashMap (internal) + Vec wire
Output stable cho client | Vec<{key_field, value_field}>
Test snapshot reliable | BTreeMap (alphabetical sort)
Metadata flexible schema | BTreeMap<String, Value> (B44 lock)
Pattern Shop API: ưu tiên Vec<{key, value}> khi key là Newtype business meaningful (UserId, ProductId, OrderId) — client parse rõ semantic. Dùng BTreeMap<String, V> khi key là string metadata flexible (tags, extra attribute). Tránh hoàn toàn HashMap<NewtypeId, V> ra wire vì phải maintain Display + FromStr impl thêm.
Size Limit Chống DoS
Khi server accept JSON body từ client, có 3 vector tấn công DoS (denial of service) chủ đạo:
- Body size attack — client gửi 1GB JSON, server allocate 1GB RAM trước khi reject; chỉ cần vài chục connection đồng thời là RAM exhaust.
- Array length attack — JSON body chỉ vài KB
[1, 2, 3, ...]triệu phần tử, Vec resize allocate hàng trăm MB; CPU spike vì serde parse mỗi element. - Nesting depth attack — JSON body cấu trúc nested chục nghìn level
[[[[...]]]], recursive parser stack overflow process crash.
Shop API lock 4 layer defense tách lớp, mỗi layer chống 1 vector + một số overlap:
Layer | Component | Chống vector | Cap
1 | tower-http RequestBodyLimitLayer | Body size (hard cap) | 2MB default
2 | AppJson custom extractor (B32) | Body size (per-request) | 2MB default
3 | DefaultBodyLimit::max(N) per-route | Body size (route-level) | 10MB import
4 | #[validate(length(min, max))] | Array length | 1000 items
Layer 1 + 2 — body size cap mặc định 2MB: axum tích hợp tower-http::limit::RequestBodyLimitLayer auto reject 413 Payload Too Large nếu body vượt cap, KHÔNG cần allocate buffer trước. AppJson lock B32 mặc định 2MB qua DefaultBodyLimit — đủ cho mọi DTO Shop API (CreateProductDto, CreateUserDto, CheckoutDto).
Layer 3 — per-route override: endpoint import bulk có thể cần body lớn hơn 2MB (vd admin import 1000 product). Dùng DefaultBodyLimit::max(N) per-route override:
use axum::extract::DefaultBodyLimit;
use axum::routing::post;
use axum::Router;
fn admin_routes() -> Router<AppState> {
Router::new()
// Import endpoint cần body lớn hơn — override 10MB
.route("/api/v1/admin/products/import", post(import_products))
.layer(DefaultBodyLimit::max(10 * 1024 * 1024))
// Endpoint khác giữ default 2MB
.merge(routes::admin_products::routes())
}
Layer 4 — validate array length: ngay cả khi body 10MB pass layer 1-3, một mảng items: Vec<ProductDto> 100 nghìn phần tử vẫn gây vấn đề khi service code iterate. Lock cap qua validator crate:
use serde::Deserialize;
use validator::Validate;
#[derive(Debug, Clone, Deserialize, Validate)]
pub struct BulkCreateProductDto {
#[validate(length(min = 1, max = 1000, message = "tối đa 1000 items mỗi request"))]
#[validate(nested)]
pub items: Vec<CreateProductDto>,
}
Validate chạy sau khi extract pass (qua ValidatedJson lock B41) — array vượt 1000 phần tử fail 422 UNPROCESSABLE_ENTITY với envelope chuẩn.
Nesting depth attack: serde_json mặc định KHÔNG cap depth, nhưng axum body limit cap 2MB đã chặn gián tiếp (nested 10K level cần body lớn hơn 2MB). Nếu cần defense cứng hơn, dùng serde_json::Deserializer::from_str(...).disable_recursion_limit() nghịch — mặc định serde_json cap recursion limit 128 level, đủ cho mọi DTO Shop API.
Lock decision Shop API:
- Default 2MB per request (lock B32 — AppJson extractor).
- 10MB override cho import/bulk endpoint qua
DefaultBodyLimit::max(10 * 1024 * 1024)per-route. - Array length cap 1000 items mỗi bulk request qua
#[validate(length(max = 1000))]. - Nesting depth dựa vào body size cap + serde_json default 128 level — đủ defense gián tiếp.
from_slice Vs from_reader — Streaming
serde_json cung cấp 3 entry point parse JSON khác nhau theo nguồn dữ liệu:
serde_json::from_str(&s: &str) -> Result<T, Error>— parse từ&strUnicode đã decode UTF-8.serde_json::from_slice(&bytes: &[u8]) -> Result<T, Error>— parse từ byte slice raw, serde_json tự decode UTF-8 inline.serde_json::from_reader(reader: R) -> Result<T, Error> where R: Read— parse từstd::io::Readtrait streaming chunk-by-chunk.
axum mặc định dùng from_slice: Json<T> extractor (và AppJson wrap lock B32) đọc toàn body request thành Bytes qua tower-http buffer, sau đó gọi serde_json::from_slice(&bytes). Trade-off:
- Pros: nhanh nhất, simple, KHÔNG bị partial parse khi network slow; body đã được tower-http buffer cap 2MB (lock B32) nên RAM bounded.
- Cons: cần buffer toàn body trước khi parse — không phù hợp file lớn vài chục MB.
serde_json::from_reader parse từ Read trait streaming, đọc chunk vài KB rồi parse incrementally:
use std::fs::File;
use std::io::BufReader;
fn parse_large_file<T: serde::de::DeserializeOwned>(path: &str) -> Result<T, Box<dyn std::error::Error>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let value = serde_json::from_reader(reader)?;
Ok(value)
}
Trade-off ngược lại:
- Pros: memory efficient cho file lớn (vài trăm MB), KHÔNG cần allocate buffer toàn file.
- Cons: slow hơn 2-3x vì syscall
readmỗi chunk, parser phải resume state cross-chunk.
Use case from_reader Shop API:
- Import CSV → convert JSON trên đĩa (B38 streaming body preview).
- Parse NDJSON (newline-delimited JSON) — mỗi dòng 1 JSON object, parse từng dòng streaming (B49 sẽ cover).
- Replay event log file lớn từ disk khi recovery.
Pattern Shop API: handler HTTP default dùng Json<T>/AppJson<T> (from_slice internal) cho mọi endpoint request body — tower-http đã buffer cap 2MB nên RAM bounded. Special case streaming file lớn dùng from_reader riêng trong service layer hoặc CLI tool, KHÔNG wire qua HTTP handler.
Apply: ProductListResponse Hoàn Chỉnh
Tổng hợp 4 collection vừa học vào DTO Product response Shop API. Extend crates/shop-common/src/dto/product.rs (cùng module với CreateProductDto B41/B44 và UpdateProductDto B42) thêm 2 struct mới:
// File: crates/shop-common/src/dto/product.rs (extend B41/B42/B44)
use std::collections::{BTreeMap, BTreeSet};
use chrono::{DateTime, Utc};
use serde::Serialize;
use serde_json::Value;
use crate::dto::{CategoryId, Money, ProductId};
#[derive(Debug, Clone, Serialize)]
pub struct ProductResponseDto {
pub id: ProductId,
pub name: String,
pub slug: String,
pub price: Money,
pub stock: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
// Vec — preserve order admin sort hiển thị
pub category_ids: Vec<CategoryId>,
// BTreeSet — unique + stable alphabetical (B47 lock)
pub tags: BTreeSet<String>,
// BTreeMap — alphabetical stable (B44 lock continued)
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, Value>,
pub created_at: DateTime<Utc>,
}
3 collection mỗi loại đại diện 1 decision lock:
category_ids: Vec<CategoryId>— admin order quan trọng (category chính đứng đầu, phụ sau).tags: BTreeSet<String>— unique (tag "apple" không lặp 2 lần) + stable wire (test snapshot, ETag cache).metadata: BTreeMap<String, Value>— flexible schema (warranty_months, color, brand) key alphabetical stable.
Envelope cho list endpoint — ProductListResponse wrap Vec<ProductResponseDto> + 4 field pagination:
use super::Pagination; // preview B23, sẽ implement deeper sau
#[derive(Debug, Clone, Serialize)]
pub struct ProductListResponse {
pub items: Vec<ProductResponseDto>,
pub total: u64,
pub page: u32,
pub per_page: u32,
pub total_pages: u32,
}
impl ProductListResponse {
pub fn new(items: Vec<ProductResponseDto>, total: u64, pagination: &Pagination) -> Self {
let total_pages = if pagination.per_page == 0 {
0
} else {
((total as f64) / (pagination.per_page as f64)).ceil() as u32
};
Self {
items,
total,
page: pagination.page,
per_page: pagination.per_page,
total_pages,
}
}
}
Note: Pagination đang lock B23 nhưng field per_page đây là preview placeholder (B23 hiện dùng size); khi B23 implement deeper sẽ thống nhất tên field per_page cho consistent với industry standard Stripe/Shopify.
Update crates/shop-common/src/dto/mod.rs re-export 2 struct mới:
// File: crates/shop-common/src/dto/mod.rs (snippet — phần B47 thêm)
pub use product::{
CreateProductDto,
ProductListResponse, // ← NEW B47
ProductResponseDto, // ← NEW B47
SLUG_REGEX,
UpdateProductDto,
};
Wire format JSON response cho endpoint GET /api/v1/products?page=1&per_page=20:
{
"items": [
{
"id": 1,
"name": "iPhone 15",
"slug": "iphone-15",
"price": "25000000.00",
"stock": 10,
"category_ids": [1, 5],
"tags": ["apple", "smartphone"],
"metadata": {"brand": "Apple", "warranty_months": 12},
"created_at": "2026-06-14T10:00:00Z"
}
],
"total": 1,
"page": 1,
"per_page": 20,
"total_pages": 1
}
Lock pattern endpoint G7 (B64 list_products implement đầy đủ): mọi list endpoint Shop API dùng Json<XxxListResponse> consistent — ProductListResponse, future OrderListResponse, CategoryListResponse, ReviewListResponse đều theo cùng pattern envelope 5 field. Phù hợp Response Type Decision Matrix lock B14.
Verify compile:
cd shop && cargo build -p shop-common
# Output:
# Compiling shop-common v0.1.0
# Finished `dev` profile [unoptimized + debuginfo] target(s)
Suggested commit:
git add crates/shop-common/src/dto/product.rs \
crates/shop-common/src/dto/mod.rs
git commit -m "B47: ProductResponseDto + ProductListResponse + collection lock (Vec/BTreeSet/BTreeMap)"
Tổng Kết
Vec<T>: ordered, allow duplicate, serialize JSON array preserve order insert — dùng cho list cart items, category_ids, products preserve order add.HashSet<T>: lookup O(1) nhanh, unique tự động, nhưng order unstable mỗi serialize — dùng internal lookup KHÔNG wire.BTreeSet<T>lock Shop API cho output wire (roles, tags) — unique + stable alphabetical order, test snapshot reliable.HashMap<K, V>: lookup O(1), key order unstable — internal only (rate limit counter, warm cache) KHÔNG serialize ra wire.BTreeMap<K, V>lock Shop API cho output wire metadata (B44 continued) — alphabetical stable, ETag cache hit ổn định, webhook signature verify đúng.- Key non-String pitfall: JSON spec RFC 8259 mục 7 chỉ chấp nhận key string; Newtype key cần impl
Display + FromStrđể round-trip (sẽ deep B58). - Alternative pattern: dùng
Vec<{key_field, value_field}>thay HashMap có key Newtype — idiomatic JSON, client parse rõ semantic. - 3 vector DoS: body size (1GB JSON), array length (triệu phần tử), nesting depth (chục nghìn level) — mỗi loại exhaust khác resource.
- 4 layer defense: RequestBodyLimit (tower-http hard cap) + AppJson lock 2MB (B32) + DefaultBodyLimit::max(N) per-route override + validate length min/max array.
- Body size lock: 2MB default mọi endpoint, 10MB override import/bulk endpoint.
- Array length cap: 1000 items mỗi bulk request qua
#[validate(length(max = 1000))]. from_sliceaxum default (body đã buffer cap 2MB, nhanh) vsfrom_readerstreaming chỉ cho file lớn special case (CSV import, NDJSON, event log replay).ProductResponseDto+ProductListResponselock fields G7 list endpoint — Vec items + total + page + per_page + total_pages.- File path lock: extend
dto/product.rs(cùng module CreateProductDto B41/B44 + UpdateProductDto B42); re-export 2 struct mới quadto/mod.rs.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Khi nào dùng
Vec<T>,HashSet<T>,BTreeSet<T>? Phân biệt 3 trường hợp theo duplicate + order + lookup speed; cho ví dụ Shop API mỗi loại. - HashMap vs BTreeMap — wire JSON output khác nhau ra sao? Cho ví dụ test snapshot fail flaky với HashMap và lý do BTreeMap fix được.
- Key non-String trong HashMap — JSON spec cho phép không? serde_json giải quyết bằng cách nào? Pattern alternative idiomatic JSON là gì và ưu thế gì?
- 4 layer defense DoS Shop API là gì? Mỗi layer chống vector tấn công nào? Tại sao cần đủ 4 layer mà không chỉ 1?
from_slicevsfrom_readerkhác nhau ra sao về memory + speed? Khi nào Shop API chọn streaming?
Đáp án
- 3 collection — phân biệt 3 chiều.
Vec<T>: ordered (preserve insert order), allow duplicate (cùng value 2 lần OK), lookup O(n) tuần tự. Use case Shop API:category_ids: Vec<CategoryId>(admin sort thứ tự hiển thị primary/secondary),cart_items: Vec<CartItem>(cùng product 2 variant khác nhau coi như 2 entry),order_items: Vec<OrderItem>(snapshot tại thời điểm checkout, order add giữ nguyên audit).pub category_ids: Vec<CategoryId>, // admin sort primary, secondary pub cart_items: Vec<CartItem>, // 2 variant coi như 2 entryHashSet<T>: unordered (mỗi serialize random vị trí), unique (duplicate silent loại), lookup O(1) nhờ hash. Use case Shop API: internal lookup KHÔNG wire — vd check role permission trong middlewareif user_roles_set.contains(&"admin".to_string())O(1) nhanh hơn Vec O(n); rate limit counterHashSet<IpAddr>blocked IP. KHÔNG serialize HashSet ra response vì order unstable gây test snapshot flaky + audit log không reproducible.// Internal lookup only, KHÔNG wire let blocked_ips: HashSet<IpAddr> = load_blocklist(); if blocked_ips.contains(&client_ip) { return Err(AppError::Forbidden); }BTreeSet<T>: ordered alphabetical (B-tree internal sort by key), unique, lookup O(log n) chậm hơn HashSet nhưng vẫn rất nhanh ở scale 1000 element. Use case Shop API: output wire MANDATORY —tags: BTreeSet<String>trên ProductResponseDto B47,roles: BTreeSet<String>trên UserResponseDto future B112,permissions: BTreeSet<String>JWT claims B116. Test snapshot stable, ETag cache hit ổn định, webhook signature verify đúng.
Quy tắc chọn: order quan trọng + có duplicate → Vec; unique + lookup nhanh internal → HashSet; unique + stable wire output → BTreeSet. Shop API lock vĩnh viễn: ưu tiên BTreeSet thay HashSet cho mọi field wire ra response.pub tags: BTreeSet<String>, // wire output, stable alphabetical - HashMap vs BTreeMap wire output. HashMap serialize JSON object key order unstable — internal hash function distribute key qua bucket dựa hash code, iterate bucket order phụ thuộc Rust version + RandomState seed (random mỗi process start). Cùng dữ liệu
{"brand": "Apple", "warranty": "12"}serialize lần 1 ra{"brand": "Apple", "warranty": "12"}, lần 2 ra{"warranty": "12", "brand": "Apple"}— JSON về semantic spec đồng nghĩa nhưng byte stream khác. Pitfall test snapshot fail flaky:
Test này fail ~50% CI run vì HashMap shuffle. Dev debug tưởng có bug, retry CI vài lần thì pass — flaky test gây mệt mỏi team, mất trust suite. BTreeMap fix được: B-tree internal sort by key alphabetical, iterate luôn theo thứ tự sort. Cùng test thay HashMap bằng BTreeMap:#[test] fn test_metadata_response() { let mut metadata = HashMap::new(); metadata.insert("brand".to_string(), "Apple".to_string()); metadata.insert("warranty".to_string(), "12".to_string()); let json = serde_json::to_string(&metadata).unwrap(); assert_eq!(json, r#"{"brand":"Apple","warranty":"12"}"#); // FAIL random }
Shop API lock vĩnh viễn (B44 continued):let mut metadata = BTreeMap::new(); metadata.insert("brand".to_string(), "Apple".to_string()); metadata.insert("warranty".to_string(), "12".to_string()); let json = serde_json::to_string(&metadata).unwrap(); assert_eq!(json, r#"{"brand":"Apple","warranty":"12"}"#); // PASS 100%BTreeMap<String, Value>MANDATORY cho mọi metadata wire output (ProductResponseDto.metadata, future UserResponseDto.preferences, OrderResponseDto.extra_attributes);HashMapchỉ cho internal lookup KHÔNG serialize. Lợi ích bổ sung BTreeMap wire: ETag tính từ body hash stable (cache hit ổn định, không invalidate vô lý), webhook signature verify Stripe-style đúng (server sign body theo BTreeMap order, client recompute cùng order match), audit log reproducible (cùng input cho cùng output JSON byte-by-byte). - Key non-String JSON spec + serde_json + alternative. JSON spec RFC 8259 mục 7 quy định "A JSON name is a sequence of zero or more Unicode characters, wrapped in quotation marks" — key trong JSON object bắt buộc string, KHÔNG cho phép integer/float/object làm key. Wire
{1: 100, 2: 200}KHÔNG phải JSON hợp lệ, parser reject ngay. Rust HashMap khác: cho phép key bất kỳ type implHash + Eq— kể cảHashMap<u64, V>hoặcHashMap<UserId, V>Newtype. serde_json giải quyết mismatch: serialize convert key sang string quaDisplaytrait (vd1u64.to_string() == "1"), deserialize parse string key về type gốc quaFromStrtrait (vd"1".parse::<u64>().unwrap() == 1u64). Round-trip hoạt động NẾU type key impl đủ 2 trait. Wire format trở thành:
Pitfall Newtype B44: UserId/ProductId/OrderId/CategoryId chỉ derive{"scores": {"1": 100, "2": 200}}Hash + Serialize + Deserializechưa cóDisplay + FromStr— compile fail khi serializeHashMap<UserId, V>với error bound trait khó debug. Lock B58 sẽ deep implDisplay + FromStrcho 5 newtype để parse Path extractor/users/:idquaPath<UserId>+ format log/error message + HashMap key conversion. Alternative pattern idiomatic JSON: dùngVec<{key_field, value_field}>thay HashMap có key Newtype:
Wire format array of object, client parse rõ semantic mỗi entry là 1 user score:#[derive(Serialize)] pub struct UserScoreEntry { pub user_id: UserId, pub score: u64, } #[derive(Serialize)] pub struct UserScores { pub scores: Vec<UserScoreEntry>, }
Ưu thế alternative: (a) KHÔNG cần impl{ "scores": [ {"user_id": 1, "score": 100}, {"user_id": 2, "score": 200} ] }Display + FromStrthêm cho Newtype (giảm boilerplate), (b) client parse rõ semantic (UserId là user_id field business meaningful, không phải key technical), (c) extend field thêm dễ — thêmrank: u32hoặcachieved_at: DateTimevào struct entry; với HashMap muốn extend phải đổi schema toàn map breaking change, (d) order stable nếu dùng Vec (HashMap key order unstable bonus pitfall). Decision matrix Shop API: ưu tiên Vec entry pattern khi key là Newtype business meaningful (UserId, ProductId, OrderId); dùng BTreeMap khi key là string metadata flexible (tags, extra attribute schemaless); HashMap chỉ internal lookup, KHÔNG wire. - 4 layer defense DoS Shop API. Tại sao cần đủ 4 layer: defense in depth nguyên lý bảo mật chuẩn — không tin 1 layer duy nhất, nếu layer bị bypass (bug, config sai, dev disable) các layer còn lại vẫn chặn được attack. Mỗi layer chống vector khác + có overlap, tăng khả năng catch attack ở stage sớm nhất. Layer 1
tower-http::limit::RequestBodyLimitLayer: hard cap body size ở mức tower-http (trước khi axum extract bất kỳ). Chống body size attack 1GB JSON — reject 413 Payload Too Large ngay khi byte vượt cap, KHÔNG allocate buffer trước. Đặt OUTER nhất trong middleware chain (lock B29 layer strategy). Cap 2MB mặc định Shop API. Layer 2AppJsoncustom extractor (lock B32): wrapaxum::Json<T>+DefaultBodyLimit::max(2 * 1024 * 1024)mặc định cho mọi endpoint dùng AppJson. Chống body size attack per-request nếu layer 1 bị bypass hoặc dev quên config tower-http layer. Cap 2MB MANDATORY mọi DTO Shop API. Layer 3DefaultBodyLimit::max(N)per-route override: endpoint đặc biệt cần body lớn hơn (admin import bulk 10MB, upload file CSV) override per-route bằng.layer(DefaultBodyLimit::max(10 * 1024 * 1024)). Chống body size attack route-level — override cao chỉ áp endpoint cần thiết, KHÔNG mở cho mọi endpoint global. Lock Shop API: 10MB cho/api/v1/admin/products/import, giữ 2MB cho route khác.
Layer 4use axum::extract::DefaultBodyLimit; Router::new() .route("/api/v1/admin/products/import", post(import_products)) .layer(DefaultBodyLimit::max(10 * 1024 * 1024)) // 10MB .merge(other_admin_routes()) // 2MB default#[validate(length(min, max))]validator crate: ngay cả khi body 10MB pass layer 1-3, mảngitems: Vec<T>100 nghìn phần tử vẫn gây vấn đề khi service code iterate. Cap qua validator crate ở mức DTO field, chạy sau khi extract pass (quaValidatedJsonlock B41) — array vượt 1000 phần tử fail 422 UNPROCESSABLE_ENTITY với envelope chuẩn.
Chống array length attack triệu phần tử trong body 10MB. Lock Shop API: cap 1000 items mỗi bulk request — đủ cho use case admin import vừa phải, ép chia batch nếu nhiều hơn (client gửi 10 batch 1000 items thay 1 request 10 nghìn items). Nesting depth attack: gián tiếp chặn bởi layer 1-3 (body 2MB không đủ 10K level nesting) + serde_json default recursion limit 128 level đủ cho mọi DTO Shop API. Defense in depth example: nếu dev mới quên áp#[derive(Deserialize, Validate)] pub struct BulkCreateDto { #[validate(length(min = 1, max = 1000, message = "tối đa 1000 items mỗi request"))] pub items: Vec<CreateProductDto>, }RequestBodyLimitLayer(layer 1 missing),AppJsondefault 2MB (layer 2) vẫn chặn body lớn ở mức extractor; nếu cả 2 missing, route layer override 10MB (layer 3) vẫn cap; nếu cả 3 missing và route accept body lớn, validator length cap 1000 items (layer 4) vẫn chặn array attack. Mỗi layer độc lập, attack phải bypass cả 4 mới reach service layer. from_slicevsfrom_reader— memory + speed.serde_json::from_slice(&bytes: &[u8]): load toàn bytes vào RAM trước, parse từ memory buffer. Memory: cần buffer toàn JSON trong RAM (vd 2MB body → 2MB allocate + parse intermediate state ~5-10MB peak). Speed: nhanh nhất, parse direct từ slice memory không syscall — typical 100-500 MB/s throughput cho JSON đơn giản. axum mặc định dùng:Json<T>extractor (vàAppJsonwrap lock B32) đọc toàn body request thànhBytesqua tower-http buffer, sau đó gọiserde_json::from_slice(&bytes). Body đã được tower-http buffer cap 2MB nên RAM bounded — safe default.// axum Jsoninternal let bytes = req.into_body().collect().await?.to_bytes(); let value: T = serde_json::from_slice(&bytes)?; serde_json::from_reader(reader: R) where R: Read: parse từstd::io::Readtrait streaming chunk-by-chunk. Memory: chỉ cần buffer vài KB chunk hiện tại + parser internal state — bounded constant memory cho file vài trăm MB. Speed: chậm hơn from_slice 2-3x vì syscallreadmỗi chunk, parser phải resume state cross-chunk; typical 50-150 MB/s throughput.
Khi nào Shop API chọn streaming: KHÔNG dùng cho HTTP handler default (tower-http đã buffer cap 2MB nên from_slice nhanh hơn + simple hơn). Dùng streaming cho 3 case special: (a) Import file lớn từ disk — admin upload CSV 100MB qua endpoint multipart B36, lưu tạmuse std::fs::File; use std::io::BufReader; let file = File::open("import.json")?; let reader = BufReader::new(file); let value: Vec<ProductDto> = serde_json::from_reader(reader)?;/tmp/import-xyz.csv, sau đó service layer convert CSV → JSON streaming qua from_reader tránh load 100MB vào RAM cùng lúc (B38 streaming body preview). (b) Parse NDJSON (newline-delimited JSON, mỗi dòng 1 JSON object) — bulk import format chuẩn cho dataset lớn (Google BigQuery, AWS Athena), parse từng dòng streaming KHÔNG load hết file (B49 sẽ cover). (c) Event log replay khi recovery — load file event log vài GB từ S3 backup, parse từng event streaming xử lý sequential. Pattern Shop API: HTTP handler defaultJson<T>/AppJson<T>(from_slice, body cap 2MB safe). Special case streaming dùngfrom_readerriêng trong service layer hoặc CLI tool (shop-cli G29 import data seed), KHÔNG wire qua HTTP handler. Lý do KHÔNG default streaming: (i) overhead chunk parse với body nhỏ KHÔNG đáng — body 100KB streaming chậm hơn from_slice 3x nhưng memory saved chỉ 95KB; (ii) error reporting from_reader khó debug hơn (chunk boundary có thể split JSON token, error message kém rõ); (iii) tower-http đã chứa body buffer cap 2MB sẵn — không có lợi ích bổ sung từ streaming cho HTTP path.
Bài Tiếp Theo
Bài 48: JSON Parse Error Detail — Line, Column, Path — error message khi parse fail có line/column/path info; serde_path_to_error crate cho path đầy đủ; custom error format gửi cho client (Stripe-style); áp parse error envelope chi tiết Shop API.
