Mục lục
- Mục Tiêu Bài Học
Extension<T>Cơ BảnExtension<T>vsState<T>— Khác Biệt Cốt Lõi- Implement X-Request-Id Middleware
- Tạo
middleware/mod.rs+ Workspace Dep - Update
router.rs— Wire Middleware - Refactor
AppError— Enrich Response Pattern - Verify Với Curl
- CurrentUser Injection Pattern (Preview B112)
- 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
Extension<T>extractor cho request-scoped data set qua middleware, extract trong handler downstream. - Phân biệt
Extension<T>(per-request, set runtime mỗi request) vsState<T>(persistent app-wide, set 1 lần lúc startup) — quyết định khi nào dùng cái nào. - Implement X-Request-Id middleware trong
crates/shop-api/src/middleware/request_id.rs— generate UUID v4 hoặc echo header client gửi, set Extension cho handler downstream, echo response header để client trace. - Implement error envelope enrich middleware đọc Extension
RequestIdrồi inject vào response JSON — refactor placeholderrequest_id: nulllock B16 sang giá trị thật. - Wire 2 middleware vào
router.rsquaaxum::middleware::from_fn(...), hiểu ordering bottom-up lock B29. - Nắm pattern CurrentUser injection qua middleware (preview B112 JWT verify) — tách concern auth khỏi handler.
- Foundation observability G15: trace context propagation OpenTelemetry cùng pattern Extension cho structured log correlation cross-service.
Extension<T> Cơ Bản
axum cung cấp Extension<T> extractor cho per-request data — dữ liệu sống đúng 1 vòng đời request, sinh ra ở middleware, tiêu thụ ở handler. Mỗi http::Request trong hyper có 1 type-map http::Extensions (HashMap key theo TypeId): middleware set qua request.extensions_mut().insert(value), handler downstream extract qua Extension(value): Extension<T>. Yêu cầu trên T: Clone + Send + Sync + 'static — Clone vì Extension extractor clone giá trị từ map sang handler arg, Send + Sync + 'static vì axum chạy multi-thread + handler có thể giữ giá trị qua await point.
// Pattern cơ bản handler dùng Extension<T>
use axum::Extension;
use axum::http::StatusCode;
#[derive(Debug, Clone)]
pub struct RequestId(pub String);
async fn handler(Extension(req_id): Extension<RequestId>) -> StatusCode {
tracing::info!(request_id = %req_id.0, "handling request");
StatusCode::OK
}
Điểm chú ý:
- Newtype
RequestId(pub String)wrapStringthay vì insert thẳngString— vìhttp::Extensionskey theoTypeId, nếu 2 middleware cùng insertStringthì middleware sau override trước. Newtype tạo type riêng biệt theoTypeIdnên co-exist nhiều extension cùng inner type được. Extension(req_id): Extension<RequestId>destructure ngay trên signature handler — pattern lock cho mọi extractor wrapper Shop API (giốngJson(payload),Path(slug),Query(pagination)).- Nếu handler khai báo
Extension<RequestId>mà middleware chưa insert trước đó → axum reject vớiExtensionRejection500 Internal Server Error (lỗi server-side config sai, không phải client lỗi). Extension<T>implementFromRequestParts(không consume body) — đặt ĐẦU arg list trước body extractor (Json,Form,Multipart) theo lock B31.
Extension<T> vs State<T> — Khác Biệt Cốt Lõi
Hai extractor nhìn giống nhau (cùng share data vào handler) nhưng khác biệt căn bản về lifecycle và thời điểm set. State<T> lock B28 dùng cho dữ liệu persistent share toàn app — DB pool, Redis pool, AppConfig, metrics handle — set 1 lần lúc startup qua Router::with_state(state). Extension<T> dùng cho dữ liệu per-request sinh runtime mỗi request — request_id (mỗi request 1 UUID khác nhau), trace context, current_user (sau auth verify).
Aspect | State<T> | Extension<T>
-------------------+---------------------------+--------------------------------
Lifecycle | App startup → shutdown | Per-request (sinh + drop mỗi req)
Set by | with_state(state) startup | request.extensions_mut().insert()
Read by | State<T> extractor | Extension<T> extractor
Use case | DB pool, Redis, config | request_id, trace, current_user
Clone-cheap | Required (Arc internal) | Required (Arc/Clone trên T)
Failure mode | Compile error router () | Runtime 500 nếu middleware quên set
Storage | Router's S type param | http::Extensions type-map per req
Quyết định khi nào dùng cái nào — quy tắc đơn giản:
- Dữ liệu giống nhau cho mọi request (cấu hình, connection pool) →
State<T>. - Dữ liệu khác nhau mỗi request (request_id unique, user khác nhau sau auth) →
Extension<T>set qua middleware. - Hybrid pattern:
State<AppState>chứauser_repo+jwt_secretpersistent, middlewarerequire_authdùng State đọc config rồi setExtension<CurrentUser>per-request — pattern lock B112.
Sai lầm phổ biến: dùng State<T> cho per-request data (vd nhồi Mutex<HashMap<Uuid, RequestContext>> vào State) → contention lock + memory leak khi quên dọn entry. Pattern Shop API lock vĩnh viễn: chỉ giữ infrastructure persistent trong AppState, dùng Extension cho mọi giá trị thay đổi per-request.
Implement X-Request-Id Middleware
Tạo file mới crates/shop-api/src/middleware/request_id.rs chứa middleware function + newtype RequestId. Pattern 4 bước MANDATORY lock vĩnh viễn cho mọi middleware tương tự sau này:
// File: crates/shop-api/src/middleware/request_id.rs
use axum::{
extract::Request,
http::HeaderValue,
middleware::Next,
response::Response,
};
use shop_common::headers::X_REQUEST_ID;
use uuid::Uuid;
/// Request-scoped identifier — newtype wrap UUID v4 hoặc giá trị client gửi.
/// Set bởi `request_id_middleware`, extract trong handler downstream qua
/// `Extension<RequestId>` (B39).
#[derive(Debug, Clone)]
pub struct RequestId(pub String);
/// Middleware đảm bảo mọi request có X-Request-Id ổn định cross-service:
/// - Echo header client gửi nếu hợp lệ (trace từ frontend qua API gateway).
/// - Generate UUID v4 mới nếu missing (server-initiated request).
/// - Set Extension cho handler downstream + error_enrich middleware (B39).
/// - Echo response header X-Request-Id để client correlation log.
pub async fn request_id_middleware(mut request: Request, next: Next) -> Response {
// Bước 1: đọc X-Request-Id từ header hoặc generate mới
let request_id = request
.headers()
.get(X_REQUEST_ID)
.and_then(|v| v.to_str().ok())
.filter(|s| !s.is_empty())
.map(String::from)
.unwrap_or_else(|| Uuid::new_v4().to_string());
// Bước 2: set Extension cho handler downstream extract qua Extension<RequestId>
request
.extensions_mut()
.insert(RequestId(request_id.clone()));
// Bước 3: chạy middleware/handler downstream
let mut response = next.run(request).await;
// Bước 4: echo X-Request-Id lên response để client correlation
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
response.headers_mut().insert(X_REQUEST_ID, header_value);
}
response
}
Phân tích từng bước:
- Bước 1:
request.headers().get(X_REQUEST_ID)dùng constantshop_common::headers::X_REQUEST_IDlock B4/B10 (header name lowercase chuẩn hyper). Chainand_then(|v| v.to_str().ok())convertHeaderValuesang&str(fail nếu chứa non-ASCII),filterloại empty string (header gửi rỗng coi như không hợp lệ),map(String::from)own giá trị. FallbackUuid::new_v4().to_string()generate UUID v4 random 122 bit entropy đủ unique cross-instance. - Bước 2:
request.extensions_mut().insert(RequestId(...))— clone string trước vì bước 4 cần dùng lại set response header.RequestIdwrap String thay vì insert thẳng String tránh xung đột TypeId với extension khác cùng inner type. - Bước 3:
next.run(request).awaitchạy phần còn lại của chain (middleware INNER hơn + handler) — trảResponseownership. - Bước 4:
HeaderValue::from_strcó thể fail nếu chứa control char — defensiveif let Ok(...)bỏ qua silent thay vì panic. Echo header cho client log correlation với backend log cùng request_id.
2 kịch bản client lock vĩnh viễn: nếu client gửi X-Request-Id: abc-123 server tôn trọng (trace từ frontend qua backend), nếu không gửi server tự generate (internal call, monitoring probe).
Tạo middleware/mod.rs + Workspace Dep
Folder crates/shop-api/src/middleware/ đã pre-allocate placeholder từ B17, file mod.rs hiện chỉ có comment. Refactor thành module aggregator + re-export top-level cho handler import 1 dòng:
// File: crates/shop-api/src/middleware/mod.rs
pub mod error_enrich;
pub mod request_id;
pub use error_enrich::enrich_error_response;
pub use request_id::{request_id_middleware, RequestId};
Add workspace dep cho UUID v4 generation. Edit shop/Cargo.toml workspace root:
# File: shop/Cargo.toml — section [workspace.dependencies]
# ... các dep cũ ...
uuid = { version = "1", features = ["v4"] }
Member crate shop-api consume qua .workspace = true. Edit crates/shop-api/Cargo.toml:
# File: crates/shop-api/Cargo.toml — section [dependencies]
# ... các dep cũ ...
uuid = { workspace = true }
Feature v4 đủ cho random UUID v4 (4-bit version + 122-bit random). Không cần feature v7 (time-ordered, dùng cho DB primary key G7 B68) hay serde (chỉ cần khi serialize UUID trong DTO — request_id ở đây luôn là String). uuid = "1" version stable từ 2022, no breaking change cho v4.
Update router.rs — Wire Middleware
Wire 2 middleware vào build_router() qua axum::middleware::from_fn(fn) — adapter wrap async function thành Layer compatibility với Router::layer(). Edit crates/shop-api/src/router.rs:
// File: crates/shop-api/src/router.rs
use axum::{
middleware as axum_middleware,
routing::get,
Router,
};
use http::StatusCode;
use crate::{handlers, middleware, routes, state::AppState};
pub fn build_router(state: AppState) -> Router {
let api_v1 = Router::new().merge(routes::products::routes());
Router::new()
.route("/", get(root))
.merge(routes::health::routes())
.merge(routes::version::routes())
.merge(routes::demo_error::routes())
.merge(routes::demo_async::routes())
.nest("/api/v1", api_v1)
.fallback(handlers::fallback::not_found)
.method_not_allowed_fallback(handlers::fallback::method_not_allowed)
// Layer apply bottom-up — outer layer ở DƯỚI chạy ĐẦU request (lock B29).
// error_enrich INNER hơn: cần Extension đã set bởi request_id.
.layer(axum_middleware::from_fn(middleware::enrich_error_response))
// request_id OUTER nhất: set Extension trước, echo header response cuối.
.layer(axum_middleware::from_fn(middleware::request_id_middleware))
.with_state(state)
}
async fn root() -> (StatusCode, &'static str) {
(StatusCode::OK, "shop-api v0.1.0")
}
Điểm chú ý ordering:
- Layer apply bottom-up (lock B29): layer ở dòng DƯỚI gần
with_statelà outer nhất — chạy đầu khi request đến, chạy cuối khi response trả về.request_id_middlewaređặt CUỐI trong code → outer nhất → set Extension trước → echo response header cuối cùng. enrich_error_responseđặt TRƯỚCrequest_id_middlewaretrong code → inner hơn → chạy SAU khirequest_id_middlewaređã set Extension → đọc đượcRequestIdtừ Extension.- Nếu đảo ngược ordering (request_id INNER, enrich OUTER) → enrich chạy trước khi Extension được set →
request.extensions().get::<RequestId>()trả None → request_id field trong envelope vẫn null. Pitfall ordering kinh điển khi compose middleware Shop API. middleware::from_fn(fn)giải đắc lực function async đơn giản — không cần impltower::Layer+tower::Serviceverbose. Đủ cho 90% middleware Shop API; chỉ cần implLayerraw khi cần state riêng cho middleware (vd rate-limit counter G17).
Refactor AppError — Enrich Response Pattern
Vấn đề kỹ thuật: impl IntoResponse for AppError lock B16 build envelope { error, code, request_id: null } với placeholder null — vì handler trả AppError, axum gọi error.into_response() với chỉ self: AppError KHÔNG có access tới Request::extensions để đọc RequestId. Đẩy Extension<RequestId> vào mọi handler arg list để tự embed request_id vào error là anti-pattern: handler signature bloat 1 arg dù 90% case không cần, lặp boilerplate 60+ endpoint, vi phạm separation of concerns.
Solution lock B16 approach (a): middleware wrap response đọc Extension, parse JSON body, inject request_id field, rebuild response. Tạo file mới crates/shop-api/src/middleware/error_enrich.rs:
// File: crates/shop-api/src/middleware/error_enrich.rs
use axum::{
body::{to_bytes, Body},
extract::Request,
http::header,
middleware::Next,
response::Response,
};
use crate::middleware::request_id::RequestId;
/// Max body size cho enrich — 1MB đủ cho mọi envelope error JSON.
/// Body lớn hơn (download stream B38) sẽ skip enrich (status không phải 4xx/5xx).
const MAX_ENRICH_BODY: usize = 1024 * 1024;
/// Middleware đọc Extension<RequestId> (set bởi request_id_middleware OUTER),
/// chạy handler, nếu response là error (4xx/5xx) + Content-Type application/json
/// thì parse body, inject field "request_id" thay placeholder null của
/// AppError::into_response (lock B16 approach (a) middleware enrich-error-body).
pub async fn enrich_error_response(request: Request, next: Next) -> Response {
// Đọc Extension trước khi consume request bởi next.run
let request_id = request
.extensions()
.get::<RequestId>()
.map(|r| r.0.clone());
let response = next.run(request).await;
let status = response.status();
let should_enrich = status.is_client_error() || status.is_server_error();
if !should_enrich {
return response;
}
let is_json = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|ct| ct.starts_with("application/json"))
.unwrap_or(false);
if !is_json {
return response;
}
let Some(request_id) = request_id else {
return response;
};
enrich_json_body(response, &request_id).await
}
async fn enrich_json_body(response: Response, request_id: &str) -> Response {
let (parts, body) = response.into_parts();
let bytes = match to_bytes(body, MAX_ENRICH_BODY).await {
Ok(b) => b,
Err(_) => return Response::from_parts(parts, Body::empty()),
};
let Ok(mut json) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
return Response::from_parts(parts, Body::from(bytes));
};
if let Some(obj) = json.as_object_mut() {
obj.insert(
"request_id".to_string(),
serde_json::Value::String(request_id.to_string()),
);
}
let new_body = match serde_json::to_vec(&json) {
Ok(v) => v,
Err(_) => return Response::from_parts(parts, Body::from(bytes)),
};
Response::from_parts(parts, Body::from(new_body))
}
Phân tích pattern:
- Đọc Extension TRƯỚC
next.run— vìnext.run(request)consume ownershipRequestsau đó không truy cậpextensions()nữa. CloneRequestId.0String sang local var. - Filter 3 điều kiện để chỉ enrich error JSON: (i) status 4xx/5xx, (ii) Content-Type bắt đầu với
application/json(tránh enrich text/plain webhook hoặc HTML), (iii) ExtensionRequestIdtồn tại (nếu middleware ordering sai thì silent fallback không panic). axum::body::to_bytes(body, MAX_ENRICH_BODY)consume body stream thànhbytes::Bytesvới cap 1MB — đủ cho envelope error JSON (typical ~100 bytes), tránh OOM nếu accidentally body lớn. Body size lớn hơn (stream download B38) status không phải 4xx/5xx nên không vào branch enrich.- Parse JSON qua
serde_json::Valuegeneric — không cần biết schema cụ thể (AppError envelope có thể thêm field tương lai nhưdetails: [...]validator B41 không phải sửa middleware).let Some(obj) = json.as_object_mut()chỉ inject khi root là object, array hay primitive bỏ qua silent. Response::from_parts(parts, Body::from(new_body))rebuild response giữ nguyên status + headers, chỉ thay body. HeaderContent-Lengthcũ có thể sai sau khi inject field — hyper sẽ tự re-compute khi serialize response (axum + hyper handle auto).- Overhead: chỉ apply cho error path 4xx/5xx (typical < 1% traffic), happy path 200 OK skip toàn bộ enrich logic. Parse + inject JSON typical 10-50μs, negligible so với DB query 1-10ms.
Verify Với Curl
Chạy server qua cargo run -p shop-api, expect log shop-api listening addr=0.0.0.0:3000. Verify 3 kịch bản lock vĩnh viễn:
Case 1 — normal request: response phải có header x-request-id với UUID v4 generate server-side.
curl -i http://localhost:3000/health
# HTTP/1.1 200 OK
# content-type: application/json
# x-request-id: 550e8400-e29b-41d4-a716-446655440000
# content-length: 15
#
# {"status":"ok"}
Case 2 — error request: envelope phải có field request_id giá trị thật không còn null placeholder lock B16.
curl -i http://localhost:3000/api/v1/unknown
# HTTP/1.1 404 Not Found
# content-type: application/json
# x-request-id: 7b9c1234-5678-90ab-cdef-1234567890ab
#
# {"error":"not found: route not found","code":"NOT_FOUND","request_id":"7b9c1234-5678-90ab-cdef-1234567890ab"}
Case 3 — client gửi X-Request-Id custom: server tôn trọng giá trị client (trace cross-service từ frontend).
curl -i http://localhost:3000/health \
-H 'X-Request-Id: my-custom-trace-id'
# HTTP/1.1 200 OK
# x-request-id: my-custom-trace-id
#
# {"status":"ok"}
# Verify cùng custom ID flow qua error envelope:
curl -i http://localhost:3000/error/not-found \
-H 'X-Request-Id: client-trace-abc'
# HTTP/1.1 404 Not Found
# x-request-id: client-trace-abc
#
# {"error":"not found: demo not found","code":"NOT_FOUND","request_id":"client-trace-abc"}
Suggested commit khi verify pass: B39: implement request_id middleware + error envelope enrich + refactor AppError request_id từ placeholder sang Extension.
CurrentUser Injection Pattern (Preview B112)
Pattern Extension áp dụng cho authentication ở B112: middleware verify JWT một lần ở edge, set CurrentUser vào Extension, handler downstream extract qua Extension<CurrentUser> KHÔNG re-verify token + KHÔNG fetch user lại trong mọi handler. Preview code (chi tiết B112 implement đầy đủ với jsonwebtoken crate):
// File: crates/shop-api/src/middleware/auth.rs (B112 implement)
use axum::{
extract::{Request, State},
middleware::Next,
response::Response,
};
use shop_common::error::{AppError, AppResult};
use crate::state::AppState;
#[derive(Debug, Clone)]
pub struct CurrentUser {
pub id: i64,
pub role: String,
}
pub async fn require_auth(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> AppResult<Response> {
// 1. Extract Bearer token từ Authorization header (typed header B33)
let token = extract_bearer_token(request.headers())?;
// 2. Verify JWT signature + claims qua jwt_secret từ AppState
let claims = verify_jwt(&token, &state.config.jwt_secret)?;
// 3. Fetch user từ DB (hoặc cache) — đảm bảo user chưa bị disable
let user = state.user_repo.find(claims.sub).await?;
// 4. Set CurrentUser vào Extension cho handler downstream
request.extensions_mut().insert(CurrentUser {
id: user.id,
role: user.role,
});
Ok(next.run(request).await)
}
// Handler tận dụng Extension không cần biết logic auth:
async fn me(
axum::Extension(user): axum::Extension<CurrentUser>,
) -> AppResult<axum::Json<serde_json::Value>> {
Ok(axum::Json(serde_json::json!({
"id": user.id,
"role": user.role,
})))
}
Pattern này lock vĩnh viễn cho Shop API:
- Tách concern clean: middleware lo authentication (parse token, verify signature, fetch user, check disabled), handler lo business logic (CRUD, validate input, query DB). Handler signature chỉ thêm 1 arg
Extension<CurrentUser>khi cần biết user. - Apply selective qua
route_layer(lock B29): endpoint public không cần auth (vdGET /api/v1/products) không wire middleware, endpoint protected (vdGET /api/v1/me) wire qua.route_layer(middleware::from_fn_with_state(state.clone(), require_auth))chỉ cho subset route. - Mở rộng cho RBAC B135:
RequireRole<"admin">extractor đọcExtension<CurrentUser>đã set bởirequire_authouter, checkuser.role == "admin", reject 403 nếu không match. Chain middleware:require_auth→require_role::<"admin">→ handler. - Mở rộng cho A/B test variant: middleware đọc feature flag service set
Extension<Variant("control" | "treatment")>, handler render UI khác nhau theo variant — same pattern.
Tổng Kết
Extension<T>request-scoped data, set qua middlewarerequest.extensions_mut().insert(value), extract quaExtension(value): Extension<T>vớiT: Clone + Send + Sync + 'static.State<T>vsExtension<T>: State persistent app-wide (set 1 lần startup quawith_state, dùng cho pool/config/metrics), Extension per-request (set runtime mỗi request qua middleware, dùng cho request_id/trace/current_user).- Implement X-Request-Id middleware 4 bước MANDATORY: đọc header client hoặc generate UUID v4 → wrap newtype
RequestId(String)insert Extension →next.run(request).await→ echo response headerX-Request-Id. - Server tôn trọng client X-Request-Id nếu hợp lệ (trace cross-service từ frontend), generate UUID v4 mới nếu missing (server-initiated request).
- Error envelope enrich pattern: middleware đọc
Extension<RequestId>, sau khinext.runnếu response 4xx/5xx + JSON → consume body quato_bytes→ parseserde_json::Value→ inject fieldrequest_idthay placeholder null lock B16 → rebuild Response quafrom_parts. - Vì sao KHÔNG impl trực tiếp trong
AppError::into_response(): handler trả AppError chỉ cóselfkhông có Request → không đọc được Extension. Solution middleware wrap response approach (a) lock B16 tránh bloat handler arg list. - Middleware ordering MANDATORY:
request_idOUTER (đăng ký SAU trong code, gầnwith_state),error_enrichINNER (đăng ký TRƯỚC) — vì layer apply bottom-up lock B29, outer chạy đầu set Extension trước, inner đọc Extension sau. - CurrentUser pattern B112: middleware
require_authextract Bearer → verify JWT → fetch user → setExtension<CurrentUser>; handler extract quaExtension(user)tách concern auth khỏi business logic. - Shop API có 2 middleware mới đầu tiên:
request_id_middleware,enrich_error_response— file path lock vĩnh viễncrates/shop-api/src/middleware/{request_id,error_enrich}.rs. - Constant
shop_common::headers::X_REQUEST_IDđã lock B4/B10 — reuse cross-middleware, không hard-code string literal. - Workspace dep thêm
uuid = { version = "1", features = ["v4"] }— generate UUID v4 random 122-bit entropy đủ unique cross-instance không cần coordinate. - Foundation observability G15: trace context propagation OpenTelemetry tương lai cùng pattern Extension; request_id attach vào tracing span cho structured log correlation cross-service (frontend log + API log + downstream microservice log cùng correlation ID).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Phân biệt
State<T>vàExtension<T>. Lifecycle mỗi loại bằng gì? Use case cụ thể Shop API mỗi loại? - X-Request-Id middleware làm 4 việc gì? Liệt kê step theo thứ tự và giải thích vì sao mỗi step cần thiết.
- Server tôn trọng X-Request-Id từ client hay generate UUID v4 mới? Tại sao tách 2 case? Cho ví dụ thực tế mỗi case.
- Pattern enrich error envelope: tại sao KHÔNG impl trực tiếp trong
AppError::into_response()? Solution mechanism dùng cái gì thay thế? Trade-off có gì? CurrentUserextractor B112 set ở đâu trong request lifecycle? Handler extract qua trait nào? Lợi ích so với pattern handler tự verify JWT mỗi endpoint?
Đáp án
- State vs Extension khác biệt cốt lõi 5 chiều: (a) Lifecycle: State sống từ app startup (lúc
app::run()build AppState) đến shutdown (SIGTERM K8s rolling deploy) — share toàn vòng đời app instance. Extension sống đúng 1 request — sinh khi middleware insert vàohttp::Extensionstype-map củaRequest, drop khi response trả xong cho client + hyper drop Request struct. (b) Set by: State set 1 lần quaRouter::with_state(state)ởbuild_router()—Stype parameter củaRouter<S>, axum embed clone State vào mỗi handler tự động khiState<T>extractor được dùng. Extension set runtime quarequest.extensions_mut().insert(value)trong middleware function — mỗi request cóhttp::Extensionstype-map riêng (HashMap key theoTypeId), middleware insert giá trị vào đó. (c) Read by: cả hai dùng extractor trong handler signature —State(value): State<T>hoặcExtension(value): Extension<T>; failure mode khác: State sai → compile error router builder (vdRouter<()>không cóState<AppState>), Extension sai → runtime 500 (middleware quên set, handler khai báo Extension reject với ExtensionRejection). (d) Use case Shop API: State cho DB poolsqlx::PgPoolG6, Redis poolfred::PoolG18, AppConfigjwt_secret/database_urlpersistent không đổi, metrics handle Prometheus G15, telemetry tracer OpenTelemetry G15 — mọi infrastructure persistent app-wide. Extension choRequestIdB39 (mỗi request 1 UUID khác),CurrentUserB112 (mỗi request user khác sau auth),TraceContextG15 OpenTelemetry span (mỗi request span riêng),FeatureFlagVariantA/B test future (mỗi request variant khác nhau theo user_id hash). (e) Clone cost: cả hai yêu cầu Clone-cheap — State typical wrapArcinternal (AppState lock B17Arc<AppConfig>), Extension typical wrapString/i64/Arc<User>nhỏ. Sai lầm pattern: dùng State cho per-request data (nhồiMutex<HashMap<Uuid, RequestContext>>vào State) → contention lock + memory leak khi quên dọn entry. Quy tắc lock Shop API: infrastructure persistent → State, dữ liệu thay đổi per-request → Extension. - X-Request-Id middleware 4 bước MANDATORY lock vĩnh viễn: (1) Đọc X-Request-Id từ header hoặc generate UUID v4 mới — chain
request.headers().get(X_REQUEST_ID).and_then(|v| v.to_str().ok()).filter(|s| !s.is_empty()).map(String::from).unwrap_or_else(|| Uuid::new_v4().to_string()). Vì sao cần: client có thể gửi request_id (frontend tự generate, API gateway forward, monitoring probe) — server tôn trọng để correlate log cross-service; nếu missing/invalid (empty string, non-ASCII char làm to_str fail) thì generate UUID v4 random 122-bit entropy đủ unique cross-instance không cần coordinate. ConstantX_REQUEST_IDtừshop_common::headerslock B4/B10 reuse cross-middleware không hard-code. (2) Set Extension RequestId(String) cho handler downstream —request.extensions_mut().insert(RequestId(request_id.clone())). Vì sao cần: handler downstream (vdfn me(Extension(req_id): Extension<RequestId>)) hoặc middleware INNER hơn (enrich_error_responseB39) cần đọc giá trị này.RequestIdnewtype wrap String thay insert thẳngStringvìhttp::Extensionskey theoTypeId— 2 middleware cùng insert String sẽ override nhau, newtype tạo TypeId riêng co-exist được. Clone vì bước 4 cần dùng lại set response header. (3) Gọinext.run(request).awaitchạy phần còn lại chain (middleware INNER + handler) — trả ownershipResponsesau khi xử lý xong. Vì sao cần: middleware là wrapper xung quanh chain, KHÔNG bypass — phải gọinext.runđể request xuôi xuống handler thực thi logic. (4) Echo X-Request-Id response header —response.headers_mut().insert(X_REQUEST_ID, HeaderValue::from_str(&request_id).unwrap_or(...)). Vì sao cần: client log tự correlate với server log qua cùng request_id — debug distributed system (frontend lỗi giờ X, tìm log backend cùng request_id giờ X xem error chi tiết).HeaderValue::from_strdefensive fail silent (control char rare) thay panic. Pattern lock vĩnh viễn cho mọi middleware tương tự Shop API (rate-limit setX-Rate-Limit-Remaining, idempotency setX-Idempotent-Replayed, version setX-Api-Version). - Server tôn trọng X-Request-Id client gửi HOẶC generate UUID v4 mới — 2 case tách bạch chiến lược: (a) Case tôn trọng client: khi
request.headers().get(X_REQUEST_ID)trả Some +to_str().ok()hợp lệ + không empty. Use case thực tế: (i) frontend SPA tự generate UUID v4 lúc user click button → gửi vớiX-Request-Id: frontend-uuid→ backend echo lại + log với request_id đó → khi user báo lỗi "đơn hàng không tạo được", support team copy request_id từ frontend console rồi grep server log tìm error trace; (ii) API gateway (Cloudflare/AWS API Gateway/Kong) sinh request_id ở edge → forward xuống backend → backend echo + log → distributed tracing 3-tier có ID nhất quán; (iii) microservice A gọi microservice B nội bộ pass request_id của request gốc → chain trace cross-service B->C->D dùng cùng correlation ID; (iv) monitoring probe Pingdom/Datadog synthetic check gửi request_id format cố địnhmonitoring-{timestamp}để tách biệt traffic monitoring khỏi user traffic. (b) Case generate UUID v4 mới: khi client KHÔNG gửi (Some + empty string hoặc None) hoặc gửi không hợp lệ (binary control char làm to_str fail). Use case: (i) developer chạycurl http://localhost:3000/healthdev test KHÔNG đặt header → server generate UUID v4 ngẫu nhiên log; (ii) browser request từ user direct (typing URL bar) không có frontend script đặt header → server generate; (iii) admin chạy script Python một lần test API không bother đặt header → server generate. Vì sao tách 2 case: (a) distributed tracing cần ID nhất quán cross-service không phải mỗi service generate lại break correlation; (b) idempotent debug: user gửi cùng request 2 lần với cùng request_id → server log 2 entries cùng ID → biết là duplicate (idempotency check B197 sẽ leverage); (c) chống pollution: nếu server LUÔN generate ignore client → mất correlation với frontend log; nếu server LUÔN tin client → client gửi UUID giả collision rủi ro grep log nhiễu. Solution hybrid: tôn trọng nếu có (trust client cho correlation), fallback generate nếu không (defensive cho dev/probe). Security consideration: client gửi request_id KHÔNG xác thực gì — không dùng request_id làm security token; chỉ dùng để correlate log/debug. Nếu cần authentication ID kèm request thì dùng JWT subject claim B116 không phải request_id. - Vì sao KHÔNG impl request_id trong
AppError::into_response():impl IntoResponse for AppErrorlock B16 có signaturefn into_response(self) -> Response— chỉ nhậnself: AppError, KHÔNG có access tớiRequest::extensions()để đọcExtension<RequestId>. axum gọierror.into_response()tự động khi handler returnErr(AppError)— vào lúc đó Request đã consume bởi extractor + handler, ownership chuyển hết, error chỉ là object riêng biệt không link tới Request gốc. Anti-pattern fix: đẩyExtension<RequestId>vào MỌI handler arg list để handler tự embed request_id vào error trước khi return — nhưng 90% handler không cần request_id cho business logic, bloat signature lặp boilerplate qua 60+ endpoint Shop API, vi phạm separation of concerns (handler tập trung business, không lo log/observability). Solution mechanism lock B16 approach (a) middleware enrich-error-body: middlewareenrich_error_responseđứng INNER hơnrequest_id_middlewaretrong chain — sau khinext.run(request).awaitnhận Response, đọc Extension đã set bởi outer (cloneRequestId.0String sang local var TRƯỚCnext.runvì sau đó request đã consume), check 3 điều kiện (status 4xx/5xx + Content-Type application/json + RequestId Some), consume body quaaxum::body::to_bytes(body, MAX_ENRICH_BODY)cap 1MB, parseserde_json::Valuegeneric (không cần biết schema chính xác),json.as_object_mut().insert("request_id", Value::String(...))inject field, serialize lạiserde_json::to_vec, rebuild quaResponse::from_parts(parts, Body::from(new_body))giữ status + headers cũ chỉ thay body. Trade-off: (+) handler signature gọn không bloat 1 arg dù không dùng (vi phạm "you pay for what you use"); (+) middleware tập trung logic enrich 1 chỗ — sau này thêm field nhưtrace_id/span_idchỉ sửa middleware, không sửa 60 handler; (+)AppError::into_responsegiữ pure không phụ thuộc context — test unit dễ; (-) overhead parse JSON ~10-50μs per error response — chỉ apply error path 4xx/5xx (typical <1% traffic), negligible so với DB query 1-10ms; (-) body cap 1MB — không issue cho envelope error nhỏ (~100 bytes), download stream B38 không vào branch vì status 200; (-) Content-Length recompute — hyper handle auto, không tự manage; (-) không inject được vàoAppError::Internalmessage string (chỉ inject envelope field) — acceptable vì client chỉ thấy generic message không leak stack trace lock B16. - CurrentUser extractor B112 set ở đâu: trong middleware
require_auth(filecrates/shop-api/src/middleware/auth.rslock B112) đặt ở vị trí OUTER trong chain trước handler downstream. Flow 4 bước: (1) extract Bearer token từAuthorizationheader quaTypedHeader<Authorization<Bearer>>lock B33 — reject 401 Unauthenticated nếu missing/malformed; (2) verify JWT signature + claims quajsonwebtoken::decodevớijwt_secretđọc từState<AppState>— reject 401 nếu signature sai, expired, audience không khớp; (3) fetch user từ DB quastate.user_repo.find(claims.sub).await— đảm bảo user chưa bị disable/deleted sau khi JWT cấp (defensive: JWT issued 7 ngày trước, user disabled hôm qua, không revoke JWT được nhưng check user DB sẽ catch); (4)request.extensions_mut().insert(CurrentUser { id, role, email, ... })set Extension cho handler downstream. Handler extract qua traitFromRequestPartsthông quaExtension<CurrentUser>—fn me(Extension(user): Extension<CurrentUser>) -> AppResult<Json<UserDto>>.Extensionđặt ĐẦU arg list trước body extractor (Json/Form) lock B31. Pattern lock vĩnh viễn cho mọi handler protected Shop API. Lợi ích so với handler tự verify JWT mỗi endpoint: (a) tách concern clean — middleware lo authentication (parse token, verify signature, fetch user, check disabled), handler lo business logic (CRUD validate input query DB) — handler signature chỉ thêm 1 argExtension<CurrentUser>khi cần biết user, không thấy Bearer token raw; (b) DRY — JWT verify logic 1 chỗ, không lặp 60+ handler — fix bug security 1 lần áp dụng toàn API; (c) performance — verify JWT (HMAC SHA256 ~50μs) + fetch user (DB query ~5ms) chạy 1 lần per request, handler nhận user cache trong Extension không re-fetch; nếu chain 5 middleware đọc Extension cùng user thì 1 fetch DB; (d) apply selective quaroute_layerlock B29 — public endpointGET /api/v1/productsKHÔNG wire middleware (anonymous OK), protected endpointGET /api/v1/mewire qua.route_layer(middleware::from_fn_with_state(state.clone(), require_auth))chỉ cho subset route — không phải mọi endpoint phải verify token; (e) mở rộng RBAC B135 —RequireRole<"admin">extractor đọcExtension<CurrentUser>đã set bởirequire_authouter, checkuser.role == "admin"reject 403 nếu không match; chain middlewarerequire_auth→require_role<"admin">→ handler; (f) testability — test handler unit passExtension<CurrentUser>mock thay phải mock toàn JWT verify + DB user_repo, tách layer test rõ ràng; (g) extend cho A/B test variant context hay tenant_id multi-tenant SaaS tương lai cùng pattern Extension không refactor handler. Trade-off duy nhất: middleware ordering phức tạp hơn (chain 3-4 middleware), debug khó hơn khi sai ordering — mitigation qua comment clusterrouter.rslock B29 + test integration verify flow end-to-end B253.
Bài Tiếp Theo
Bài 40: Response Builder — Custom IntoResponse — bài CUỐI Group 4 Extractors Và Response Sâu: đi sâu impl IntoResponse cho tuple (StatusCode, HeaderMap, Json<T>) 3-element pattern lock B14 cho 201 Created + Location header, builder pattern wrap response qua Response::builder() hyper, optional ApiResponse<T> wrapper envelope đã preview B14 cho data endpoint consistent { data, meta, links } JSON:API style hoặc { items, total, page } ListResponse style lock B23, decision matrix per endpoint pattern Shop API response envelope đầy đủ kết thúc Group 4 sẵn sàng vào Group 5 JSON Body Streaming chuyển sang validator crate B41 + body size limit B47 + compression B48.
