Danh sách bài viết

Bài 39: Extension Extractor — Request-Scoped Data

Bài 39 của series Rust RESTful API — đi sâu axum::Extension<T> extractor cho request-scoped data (dữ liệu sống đúng 1 vòng đời request, khác State<T> share toàn ứng dụng) qua pattern middleware set giá trị vào request.extensions_mut().insert(value) rồi handler downstream extract qua Extension(value): Extension<T> đòi T: Clone + Send + Sync + 'static; cốt lõi khác biệt với State<T> lock B28: State set 1 lần lúc app startup qua Router::with_state(state) dùng cho pool DB / Redis / config / metrics handle persistent app-wide, Extension set runtime mỗi request qua middleware dùng cho request_id (unique mỗi request), trace context OpenTelemetry, current_user (per session sau auth verify); bài này CODE THỰC TẾ — implement 2 middleware mới đầu tiên cho Shop API hoàn thiện request_id flow đã lock placeholder từ B16. Implement X-Request-Id middleware lock vĩnh viễn trong file mới crates/shop-api/src/middleware/request_id.rs: function request_id_middleware 4 bước MANDATORY — (1) đọc header X-Request-Id client gửi qua request.headers().get(X_REQUEST_ID) dùng constant shop_common::headers::X_REQUEST_ID lock B4/B10, nếu missing/invalid thì generate UUID v4 mới qua Uuid::new_v4().to_string(); (2) wrap thành newtype RequestId(pub String) derive Clone + insert vào request.extensions_mut().insert(RequestId(...)) cho handler downstream extract qua Extension<RequestId>; (3) gọi next.run(request).await chạy middleware/handler downstream; (4) echo response header X-Request-Id giá trị tương ứng qua response.headers_mut().insert(X_REQUEST_ID, header_value) để client correlation log cross-service. Hai kịch bản client: nếu client gửi X-Request-Id: my-trace-id thì server tôn trọng (trace cross-service từ frontend qua API gateway qua backend), nếu không gửi thì server tự generate (server-initiated request, internal call). Implement error envelope enrich middleware lock vĩnh viễn trong file mới crates/shop-api/src/middleware/error_enrich.rs giải pitfall: impl IntoResponse for AppError lock B16 KHÔNG có access tới Request::extensions (handler nhận self: AppError không nhận Request) → không thể tự đọc Extension<RequestId> trực tiếp; solution mechanism middleware enrich-error-body approach (a) lock B16: middleware đứng INNER hơn request_id_middleware đọc RequestId từ Extension đã set bởi outer, gọi next.run(request).await nhận response, check status is_client_error() || is_server_error() + Content-Type application/json → consume body qua axum::body::to_bytes(body, 1024 * 1024).await, parse serde_json::Value, inject field "request_id" thay placeholder null lock B16, serialize lại body qua serde_json::to_vec, rebuild Response qua Response::from_parts(parts, Body::from(new_body)). Approach này lock vĩnh viễn KHÔNG đẩy Extension<RequestId> vào mọi handler arg list (trade-off: handler signature gọn không bloat 1 arg dù không dùng, middleware tập trung logic enrich 1 chỗ, body parse JSON overhead ~10μs chỉ apply cho error path 4xx/5xx không ảnh hưởng happy path 200). Wire 2 middleware vào router.rs qua axum::middleware::from_fn(request_id_middleware) + from_fn(enrich_error_response) — function wrap thành Layer compatibility với Router::layer(); ordering MANDATORY request_id OUTER (đăng ký SAU) → error_enrich INNER (đăng ký TRƯỚC) vì axum layer chain apply bottom-up (outer layer cuối chain chạy đầu request — lock B29) → request_id_middleware chạy đầu set Extension trước, error_enrich chạy sau đọc Extension đã có. Pattern CurrentUser injection preview B112: middleware require_auth extract Bearer token từ header → verify JWT qua jsonwebtoken crate B112 → fetch user từ state.user_repo → set CurrentUser { id, role } vào Extension → handler extract qua Extension(user): Extension<CurrentUser> KHÔNG re-verify token + KHÔNG fetch user lại trong mọi handler (tách concern clean: middleware lo authentication, handler lo business logic). Pattern này áp dụng cho RequireAuth B14 baseline + RequireRole<"admin"> B135 admin endpoint + future A/B test variant context inject từ middleware feature flag service. Foundation observability G15: trace context propagation OpenTelemetry tương lai cùng pattern Extension — middleware tracing::Span::current().record("request_id", &req_id) attach request_id vào tracing span cho structured log correlation cross-service, X-Request-Id header echo back client để frontend log + backend log + downstream microservice log cùng correlation ID dễ debug distributed system. Workspace state change: 3 file thay đổi — NEW crates/shop-api/src/middleware/mod.rs re-export 2 middleware + RequestId struct, NEW crates/shop-api/src/middleware/request_id.rs implement request_id_middleware + RequestId(pub String) newtype, NEW crates/shop-api/src/middleware/error_enrich.rs implement enrich_error_response + helper enrich_json_body, UPDATED crates/shop-api/src/router.rs add 2 dòng .layer(middleware::from_fn(...)) bottom của chain trước with_state, UPDATED workspace root Cargo.toml add uuid = { version = "1", features = ["v4"] } vào workspace.dependencies, UPDATED crates/shop-api/Cargo.toml add uuid.workspace = true. Verify qua 3 case curl: (1) normal request GET /health → response có header x-request-id: <uuid-v4>; (2) error request GET /api/v1/unknown → 404 + envelope {error, code: "NOT_FOUND", request_id: "<uuid>"} field thật KHÔNG còn null; (3) client gửi X-Request-Id: my-custom-id → server echo lại đúng giá trị đó. Suggested commit: B39: implement request_id middleware + error envelope enrich + refactor AppError request_id từ placeholder sang Extension.

14/06/2026
11 phút đọc
0 lượt xem
1

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) vs State<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 RequestId rồi inject vào response JSON — refactor placeholder request_id: null lock B16 sang giá trị thật.
  • Wire 2 middleware vào router.rs qua axum::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.
2

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 + 'staticCloneExtension 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) wrap String thay vì insert thẳng String — vì http::Extensions key theo TypeId, nếu 2 middleware cùng insert String thì middleware sau override trước. Newtype tạo type riêng biệt theo TypeId nê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ống Json(payload), Path(slug), Query(pagination)).
  • Nếu handler khai báo Extension<RequestId> mà middleware chưa insert trước đó → axum reject với ExtensionRejection 500 Internal Server Error (lỗi server-side config sai, không phải client lỗi).
  • Extension<T> implement FromRequestParts (không consume body) — đặt ĐẦU arg list trước body extractor (Json, Form, Multipart) theo lock B31.
3

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ề lifecyclethờ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ứa user_repo + jwt_secret persistent, middleware require_auth dùng State đọc config rồi set Extension<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.

4

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 constant shop_common::headers::X_REQUEST_ID lock B4/B10 (header name lowercase chuẩn hyper). Chain and_then(|v| v.to_str().ok()) convert HeaderValue sang &str (fail nếu chứa non-ASCII), filter loại empty string (header gửi rỗng coi như không hợp lệ), map(String::from) own giá trị. Fallback Uuid::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. RequestId wrap 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).await chạy phần còn lại của chain (middleware INNER hơn + handler) — trả Response ownership.
  • Bước 4: HeaderValue::from_str có thể fail nếu chứa control char — defensive if 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).

5

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.

6

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_stateouter 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ƯỚC request_id_middleware trong code → inner hơn → chạy SAU khi request_id_middleware đã set Extension → đọc được RequestId từ 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 impl tower::Layer + tower::Service verbose. Đủ cho 90% middleware Shop API; chỉ cần impl Layer raw khi cần state riêng cho middleware (vd rate-limit counter G17).
7

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 ownership Request sau đó không truy cập extensions() nữa. Clone RequestId.0 String 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) Extension RequestId tồ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ành bytes::Bytes vớ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::Value generic — 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. Header Content-Length cũ 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.
8

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.

9

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 (vd GET /api/v1/products) không wire middleware, endpoint protected (vd GET /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 đọc Extension<CurrentUser> đã set bởi require_auth outer, check user.role == "admin", reject 403 nếu không match. Chain middleware: require_authrequire_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.
10

Tổng Kết

  • Extension<T> request-scoped data, set qua middleware request.extensions_mut().insert(value), extract qua Extension(value): Extension<T> với T: Clone + Send + Sync + 'static.
  • State<T> vs Extension<T>: State persistent app-wide (set 1 lần startup qua with_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 header X-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 khi next.run nếu response 4xx/5xx + JSON → consume body qua to_bytes → parse serde_json::Value → inject field request_id thay placeholder null lock B16 → rebuild Response qua from_parts.
  • Vì sao KHÔNG impl trực tiếp trong AppError::into_response(): handler trả AppError chỉ có self khô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_id OUTER (đăng ký SAU trong code, gần with_state), error_enrich INNER (đă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_auth extract Bearer → verify JWT → fetch user → set Extension<CurrentUser>; handler extract qua Extension(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ễn crates/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).
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Phân biệt State<T>Extension<T>. Lifecycle mỗi loại bằng gì? Use case cụ thể Shop API mỗi loại?
  2. 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.
  3. 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.
  4. 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ì?
  5. CurrentUser extractor 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
  1. 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ào http::Extensions type-map của Request, drop khi response trả xong cho client + hyper drop Request struct. (b) Set by: State set 1 lần qua Router::with_state(state)build_router()S type parameter của Router<S>, axum embed clone State vào mỗi handler tự động khi State<T> extractor được dùng. Extension set runtime qua request.extensions_mut().insert(value) trong middleware function — mỗi request có http::Extensions type-map riêng (HashMap key theo TypeId), middleware insert giá trị vào đó. (c) Read by: cả hai dùng extractor trong handler signature — State(value): State<T> hoặc Extension(value): Extension<T>; failure mode khác: State sai → compile error router builder (vd Router<()> 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 pool sqlx::PgPool G6, Redis pool fred::Pool G18, AppConfig jwt_secret/database_url persistent không đổi, metrics handle Prometheus G15, telemetry tracer OpenTelemetry G15 — mọi infrastructure persistent app-wide. Extension cho RequestId B39 (mỗi request 1 UUID khác), CurrentUser B112 (mỗi request user khác sau auth), TraceContext G15 OpenTelemetry span (mỗi request span riêng), FeatureFlagVariant A/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 wrap Arc internal (AppState lock B17 Arc<AppConfig>), Extension typical wrap String/i64/Arc<User> nhỏ. Sai lầm pattern: dùng State cho per-request data (nhồi Mutex<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.
  2. 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. Constant X_REQUEST_ID từ shop_common::headers lock B4/B10 reuse cross-middleware không hard-code. (2) Set Extension RequestId(String) cho handler downstreamrequest.extensions_mut().insert(RequestId(request_id.clone())). Vì sao cần: handler downstream (vd fn me(Extension(req_id): Extension<RequestId>)) hoặc middleware INNER hơn (enrich_error_response B39) cần đọc giá trị này. RequestId newtype wrap String thay insert thẳng Stringhttp::Extensions key theo TypeId — 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ọi next.run(request).await chạy phần còn lại chain (middleware INNER + handler) — trả ownership Response sau khi xử lý xong. Vì sao cần: middleware là wrapper xung quanh chain, KHÔNG bypass — phải gọi next.run để request xuôi xuống handler thực thi logic. (4) Echo X-Request-Id response headerresponse.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_str defensive fail silent (control char rare) thay panic. Pattern lock vĩnh viễn cho mọi middleware tương tự Shop API (rate-limit set X-Rate-Limit-Remaining, idempotency set X-Idempotent-Replayed, version set X-Api-Version).
  3. 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ới X-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ố định monitoring-{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ạy curl http://localhost:3000/health dev 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.
  4. Vì sao KHÔNG impl request_id trong AppError::into_response(): impl IntoResponse for AppError lock B16 có signature fn into_response(self) -> Response — chỉ nhận self: AppError, KHÔNG có access tới Request::extensions() để đọc Extension<RequestId>. axum gọi error.into_response() tự động khi handler return Err(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: đẩy Extension<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: middleware enrich_error_response đứng INNER hơn request_id_middleware trong chain — sau khi next.run(request).await nhận Response, đọc Extension đã set bởi outer (clone RequestId.0 String sang local var TRƯỚC next.run vì sau đó request đã consume), check 3 điều kiện (status 4xx/5xx + Content-Type application/json + RequestId Some), consume body qua axum::body::to_bytes(body, MAX_ENRICH_BODY) cap 1MB, parse serde_json::Value generic (không cần biết schema chính xác), json.as_object_mut().insert("request_id", Value::String(...)) inject field, serialize lại serde_json::to_vec, rebuild qua Response::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_id chỉ sửa middleware, không sửa 60 handler; (+) AppError::into_response giữ 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ào AppError::Internal message string (chỉ inject envelope field) — acceptable vì client chỉ thấy generic message không leak stack trace lock B16.
  5. CurrentUser extractor B112 set ở đâu: trong middleware require_auth (file crates/shop-api/src/middleware/auth.rs lock B112) đặt ở vị trí OUTER trong chain trước handler downstream. Flow 4 bước: (1) extract Bearer token từ Authorization header qua TypedHeader<Authorization<Bearer>> lock B33 — reject 401 Unauthenticated nếu missing/malformed; (2) verify JWT signature + claims qua jsonwebtoken::decode với jwt_secret đọc từ State<AppState> — reject 401 nếu signature sai, expired, audience không khớp; (3) fetch user từ DB qua state.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 trait FromRequestParts thông qua Extension<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 arg Extension<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 qua route_layer lock B29 — public endpoint GET /api/v1/products KHÔNG wire middleware (anonymous OK), protected endpoint GET /api/v1/me wire 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 B135RequireRole<"admin"> extractor đọc Extension<CurrentUser> đã set bởi require_auth outer, check user.role == "admin" reject 403 nếu không match; chain middleware require_authrequire_role<"admin"> → handler; (f) testability — test handler unit pass Extension<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 cluster router.rs lock B29 + test integration verify flow end-to-end B253.
12

Bài Tiếp Theo

— 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.