Danh sách bài viết

Bài 31: Extractor Trait — Bản Chất

Bài 31 của series Rust RESTful API — bài ĐẦU Group 4 Extractors Và Response Sâu. Extractor (bộ trích xuất) trong axum là cơ chế parse argument cho handler từ HTTP request: mỗi arg của handler là 1 extractor, axum tự động gọi trait method tương ứng để extract giá trị, fail nhanh khi 1 extractor không parse được. Bản chất gói gọn trong 2 trait cốt lõi của module axum::extract: FromRequestParts<S> — extract từ request parts (method, URI, headers, path params, query string, extension map) KHÔNG consume body, áp dụng cho Path<T>, Query<T>, State<S>, TypedHeader<H>, HeaderMap, Extension<T> — handler có thể có nhiều extractor loại này cùng lúc; FromRequest<S, B> — extract TOÀN BỘ request bao gồm body stream, áp dụng cho Json<T>, Form<T>, Bytes, String, Multipart — handler CHỈ được 1 extractor body vì body là single-pass stream đọc xong là hết. Execution order: axum chạy mỗi extractor tuần tự từ trái sang phải theo arg list, await riêng từng extractor, fail thì STOP và return rejection (axum dispatch qua IntoResponse ra HTTP error), nếu State fail handler không chạy, nếu Json parse fail thì Path/Query đã extract OK vẫn discard. Quy tắc body extractor PHẢI đặt CUỐI arg list: đảo order Json trước Path compile error "the trait FromRequest is not implemented for X" vì body extractor consume Request<B> toàn bộ, không còn parts cho extractor sau. Convention Shop API lock B22 vĩnh viễn: State → Path → Query → TypedHeader/HeaderMap → Extension → body — mọi handler từ G7 onward tuân thủ thứ tự này. Rejection type: mỗi extractor có associated type Rejection: IntoResponse default trả 422 cho Json parse fail và 400 cho Path/Query parse fail với generic message, Shop API muốn override rejection thành AppError::BadRequest hoặc AppError::Validation với envelope chuẩn lock B16 — pattern custom wrapper extractor AppPath<T>/AppQuery<T>/AppJson<T> sẽ implement đầy đủ ở B32 trong folder crates/shop-api/src/extractors/ (placeholder ready từ B17). State<S> là extractor đặc biệt: clone state đã .with_state() set vào Router, KHÔNG thực sự "parse" request, rejection effectively unreachable nếu Router setup đúng. B31 conceptual deep dive, foundation cho B32-B40 implement custom extractor cụ thể — KHÔNG thay đổi workspace state.

14/06/2026
10 phút đọc
2 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu 2 trait cốt lõi của module axum::extract: FromRequestParts<S> (extract từ request parts không consume body) và FromRequest<S, B> (extract toàn bộ request bao gồm body).
  • Biết khi nào dùng trait nào — body consumer (Json/Form/Bytes/Multipart) impl FromRequest, mọi extractor còn lại (Path/Query/State/TypedHeader/Extension) impl FromRequestParts.
  • Nắm execution order: axum chạy mỗi extractor tuần tự từ trái sang phải theo arg list, await riêng, fail nhanh khi 1 extractor return rejection STOP không chạy tiếp.
  • Biết tại sao body extractor PHẢI đặt CUỐI arg list — body là single-pass stream, consume xong là hết, đảo order compile error.
  • Hiểu rejection type cho mỗi extractor — associated type Rejection: IntoResponse default trả 422 (Json parse fail) hoặc 400 (Path/Query parse fail) với generic message; Shop API muốn override thành AppError envelope chuẩn lock B16.
  • Chuẩn bị implement custom wrapper AppPath<T>, AppQuery<T>, AppJson<T> ở B32 trong folder crates/shop-api/src/extractors/ (placeholder ready từ B17).
2

2 Trait Cốt Lõi: FromRequestParts Và FromRequest

Module axum::extract định nghĩa 2 trait phân biệt theo tiêu chí có consume body hay không. Bản chất rất ngắn:

// File: axum-core/src/extract/mod.rs (rút gọn)
pub trait FromRequestParts<S>: Sized {
    type Rejection: IntoResponse;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &S,
    ) -> Result<Self, Self::Rejection>;
}

pub trait FromRequest<S, B = Body>: Sized {
    type Rejection: IntoResponse;

    async fn from_request(
        req: Request<B>,
        state: &S,
    ) -> Result<Self, Self::Rejection>;
}

FromRequestParts<S> — extract từ parts của request: method, uri, version, headers, extensions, path params (parse từ uri), query string. Nhận &mut Parts mutable reference (cho phép sửa headers nếu cần), KHÔNG đụng đến body. Generic S là kiểu state lock vào Router qua .with_state(state) — extractor dùng &S để truy cập config, pool, secret. Built-in implementor phổ biến:

  • Path<T> — parse path param (vd /users/:id) từ uri
  • Query<T> — parse query string từ uri
  • State<S> — clone state đã lock vào Router
  • TypedHeader<H> — parse typed header (Authorization, ContentType) từ headers
  • HeaderMap — toàn bộ header map raw
  • Extension<T> — đọc giá trị inject từ middleware vào extensions map

Handler có thể có nhiều extractor loại FromRequestParts cùng lúc — axum gọi from_request_parts cho từng arg theo thứ tự, mỗi lần với &mut Parts shared (parts vẫn còn cho extractor sau).

FromRequest<S, B> — extract TOÀN BỘ request bao gồm body. Nhận Request<B> owned (consume), không trả lại được. Đây là điểm khác biệt then chốt: chỉ 1 extractor body được phép trong handler vì sau khi consume body không còn để extractor khác đọc. Generic B = Body mặc định là axum::body::Body (stream). Built-in implementor:

  • Json<T> — deserialize body application/json sang T: DeserializeOwned
  • Form<T> — deserialize body application/x-www-form-urlencoded
  • Bytes — đọc body raw thành Bytes
  • String — đọc body UTF-8 thành String
  • Multipart — stream multipart/form-data theo field

axum cung cấp blanket impl: nếu T: FromRequestParts<S> thì tự động T: FromRequest<S, B> — cho phép Path/Query/State đứng cạnh Json trong cùng handler. Blanket impl này chỉ work theo chiều một, body extractor KHÔNG được tự động trở thành FromRequestParts vì cần consume body.

3

Execution Order: Tuần Tự + Fail Nhanh

axum extract argument theo thứ tự tuần tự từ trái sang phải — không parallel, không lazy. Mỗi extractor await riêng, fail thì STOP và return rejection cho client. Flow conceptual:

incoming Request
       │
       ▼
   split Parts + Body
       │
       ▼
   loop arg list trái → phải:
     arg[0] (FromRequestParts) → await from_request_parts(&mut parts, &state)
        ├─ Err(rejection) → STOP, return rejection.into_response()
        └─ Ok(value)      → tiếp tục
     arg[1] (FromRequestParts) → await from_request_parts(...)
        ├─ Err → STOP
        └─ Ok  → tiếp tục
     ...
     arg[N] (FromRequest body, CUỐI) → await from_request(req_with_body, &state)
        ├─ Err → STOP
        └─ Ok  → handler chạy với mọi arg đã extract
       │
       ▼
   handler(arg[0], arg[1], ..., arg[N]) → Response

Pattern handler chuẩn theo flow trên:

// File: crates/shop-api/src/handlers/products.rs (preview G7)
async fn update_product(
    State(state): State<AppState>,           // 1. extract State (clone)
    Path(slug): Path<String>,                 // 2. extract Path param
    Query(version): Query<UpdateVersion>,     // 3. extract Query string
    Json(dto): Json<UpdateProductDto>,        // 4. extract Json body (CUỐI)
) -> AppResult<Json<ProductDto>> {
    // chỉ chạy nếu cả 4 extractor đều Ok
    ...
}

Một số hệ quả thực tế của model fail-fast:

  • State fail → handler không chạy: nếu state setup sai (vd quên .with_state() ở root Router) extractor State<AppState> fail compile-time — chứ không phải runtime; điều này khiến rejection của State effectively unreachable trong code đã build (chi tiết bước 8).
  • Path fail → Query/Json không extract: vd path /products/abc mà handler khai báo Path<i64>, Path return rejection 400, handler skip hoàn toàn, body request bị axum drop (chưa kịp đọc).
  • Json parse fail → Path/Query đã extract OK vẫn discard: arg đã extract trước nằm trong scope stack frame, return rejection tức là discard mọi giá trị đã extract — đây là chi phí cố hữu của model tuần tự, nhưng đổi lại đảm bảo handler chỉ chạy khi mọi arg hợp lệ.
  • Không có short-circuit thông minh: axum không nhìn signature trước để skip body extract khi Path fail — nó luôn extract theo thứ tự, fail bất cứ đâu cũng STOP ngay.

Model này đơn giản, predictable, dễ debug. Trade-off nhỏ (discard giá trị đã extract khi extractor sau fail) chấp nhận được vì extractor parts cheap (parse query/path là chuỗi vài byte), chi phí thật chỉ nằm ở Json body deserialize.

4

Body Extractor PHẢI Đặt CUỐI

Quy tắc bất biến: body extractor (impl FromRequest) PHẢI là arg CUỐI trong handler. Lý do nằm ở 2 sự thật về body HTTP:

  • Body là single-pass stream — đọc xong là hết, không seek lại được. axum body type axum::body::Body là wrapper quanh http_body::Body stream chunk, mỗi chunk consume một lần.
  • FromRequest<S, B> nhận Request<B> owned — consume cả parts lẫn body. Sau khi gọi extractor này không còn gì cho extractor sau.

Anti-pattern đảo order Json trước Path:

// SAI — compile error
async fn update_product(
    Json(dto): Json<UpdateProductDto>,        // body extractor TRƯỚC
    Path(slug): Path<String>,                 // path extractor SAU → compile error
) -> AppResult<Json<ProductDto>> {
    ...
}

Compile error message điển hình từ rustc:

error[E0277]: the trait bound `fn(Json<UpdateProductDto>, Path<String>) -> ...
              {update_product}: Handler<_, _>` is not satisfied
   --> src/routes/products.rs:42:30
    |
 42 |     .route("/:slug", put(update_product))
    |                      --- ^^^^^^^^^^^^^^
    |                      |
    |                      required by a bound introduced by this call
    |
    = help: the trait `Handler<T, S>` is implemented for fns that take extractors
            with `FromRequest` as the LAST argument
    = note: the trait `FromRequestParts` is not implemented for `Json<UpdateProductDto>`

rustc nói thẳng: "the trait FromRequestParts is not implemented for Json<...>" — vì Json chỉ impl FromRequest (consume body), không impl FromRequestParts. Trait bound của Handler trong axum yêu cầu mọi arg trừ arg cuối phải impl FromRequestParts, arg cuối có thể impl một trong hai. Đặt Json không phải cuối vi phạm bound này.

Fix: đảo order, body cuối:

// ĐÚNG — compile pass
async fn update_product(
    Path(slug): Path<String>,                 // path extractor TRƯỚC
    Json(dto): Json<UpdateProductDto>,        // body extractor CUỐI
) -> AppResult<Json<ProductDto>> {
    ...
}

Hệ quả: 1 handler chỉ được 1 extractor body. Cần parse multipart + json cùng request? Không hợp lệ — multipart request có body multipart, json request có body application/json, một request không thể có cả hai. Cần 2 dạng body khác nhau cho cùng endpoint? Tách 2 route riêng hoặc dùng Multipart với 1 field là json string.

Pitfall thường gặp khi mới học axum: copy code từ Express/Spring sang đặt body đầu (vì các framework đó dùng decorator/annotation không strict order). axum strict — compiler enforce, không bao giờ runtime bug.

5

Convention Shop API Lock (B22 Confirm)

Compiler chỉ enforce body cuối. Còn lại Path/Query/State/TypedHeader/Extension đứng đâu trong arg list cũng compile pass (đều impl FromRequestParts). Để giữ codebase nhất quán, Shop API lock convention vĩnh viễn từ B22:

State → Path → Query → TypedHeader/HeaderMap → Extension → body

Code handler chuẩn áp dụng full convention:

// File: crates/shop-api/src/handlers/products.rs (preview G14)
use axum::{
    extract::{Path, Query, State},
    Json,
};
use axum_extra::{
    headers::{authorization::Bearer, Authorization},
    TypedHeader,
};
use axum::Extension;

async fn update_product(
    State(state): State<AppState>,                              // 1. shared resource
    Path(slug): Path<String>,                                   // 2. path param
    Query(version): Query<UpdateVersion>,                       // 3. query string
    TypedHeader(auth): TypedHeader<Authorization<Bearer>>,       // 4. typed header
    Extension(user): Extension<CurrentUser>,                    // 5. middleware-injected
    Json(dto): Json<UpdateProductDto>,                          // 6. body LAST
) -> AppResult<Json<ProductDto>> {
    ...
}

Lý do convention theo thứ tự này:

  • State đầu — đọc trái sang phải biết handler cần resource gì rồi mới đến input client. Convention này khớp với pattern từ web framework khác: Spring @Autowired first, NestJS @Inject first (lock B28).
  • Path trước Query — path là identity của resource (/products/:slug), query là filter/option trên resource đó. Đọc handler signature có cảm giác "URL → option" tự nhiên.
  • TypedHeader/HeaderMap trước Extension — header là dữ liệu client gửi trực tiếp, Extension là dữ liệu middleware inject (thường sau khi parse header). Thứ tự này phản ánh data flow: client header → middleware xử lý → extension.
  • Extension trước body — Extension là metadata về request (user identity, request id, trace context), body là payload nghiệp vụ. Convention "metadata trước payload" giúp đọc handler signature hiểu context trước khi vào nội dung.
  • Body cuối — compiler enforce, không lựa chọn khác.

Mọi handler Shop API từ G7 onward tuân thủ convention này. Code review reject PR nếu order sai — tiêu chuẩn coding cứng cho team.

6

Rejection Type — Lỗi Khi Extract Fail

Mỗi extractor có associated type Rejection: IntoResponse — kiểu trả khi parse fail. axum tự động gọi rejection.into_response() ra HTTP error cho client, không cần handler xử lý.

Default rejection của 3 extractor phổ biến nhất:

  • Json<T> rejection → JsonRejection với 4 variant (MissingJsonContentType, JsonDataError, JsonSyntaxError, BytesRejection); default into_response trả 422 Unprocessable Entity hoặc 415 Unsupported Media Type với body text generic vd "Failed to deserialize the JSON body into the target type: missing field `name` at line 3 column 5".
  • Path<T> rejection → PathRejection; default trả 400 Bad Request với body text generic vd "Invalid URL: Cannot parse `abc` to a `i64`".
  • Query<T> rejection → QueryRejection; default trả 400 Bad Request với body text generic vd "Failed to deserialize query string: missing field `page`".

Default behavior này functional cho prototype nhưng không đủ cho production Shop API vì 2 lý do:

  • Format response không nhất quán — endpoint thành công trả Json envelope {data, meta} (lock B14), endpoint extract fail trả text/plain raw. Client phải xử lý 2 format khác nhau cho cùng endpoint.
  • Mất context envelope chuẩn — Shop API lock AppError enum trong shop-common::error (B16) với 11 variant ánh xạ HTTP status, mỗi error response có shape {error: {code, message, details}}. Extractor default bypass envelope này.

Giải pháp: custom wrapper extractor — Shop API định nghĩa AppPath<T>, AppQuery<T>, AppJson<T> bọc bên ngoài Path/Query/Json gốc, override type Rejection = AppError. Handler dùng wrapper thay vì extractor gốc — rejection tự động ra AppError::BadRequest hoặc AppError::Validation với envelope chuẩn.

Pattern này không loại bỏ extractor gốc — nó delegate parse logic cho extractor gốc, chỉ map rejection sang AppError. Code complete sẽ implement đầy đủ ở B32.

7

Custom Extractor Workflow (Preview B32)

Workflow tạo custom extractor gồm 3 bước cố định:

  • Tạo wrapper struct generic pub struct AppXxx<T>(pub T); — tuple struct với 1 field public để destructure trong handler.
  • Impl trait tương ứng (FromRequestParts hoặc FromRequest) cho wrapper, với type Rejection = AppError.
  • Trong impl, gọi extractor gốc (Path/Query/Json) làm parse logic, map rejection của extractor gốc sang AppError variant phù hợp.

Preview implementation AppPath<T> (chi tiết đầy đủ B32):

// File: crates/shop-api/src/extractors/path.rs (preview B32)
use axum::{
    extract::{FromRequestParts, Path},
    http::request::Parts,
};
use serde::de::DeserializeOwned;
use shop_common::error::AppError;

pub struct AppPath<T>(pub T);

impl<S, T> FromRequestParts<S> for AppPath<T>
where
    T: DeserializeOwned + Send,
    S: Send + Sync,
{
    type Rejection = AppError;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &S,
    ) -> Result<Self, AppError> {
        match Path::<T>::from_request_parts(parts, state).await {
            Ok(Path(value)) => Ok(AppPath(value)),
            Err(rejection) => Err(AppError::BadRequest(rejection.body_text())),
        }
    }
}

Handler dùng wrapper:

// File: crates/shop-api/src/handlers/products.rs (preview G7)
use crate::extractors::AppPath;

async fn get_product(
    State(state): State<AppState>,
    AppPath(slug): AppPath<String>,    // ← thay Path<String>
) -> AppResult<Json<ProductDto>> {
    let product = state.product_repo.get_by_slug(&slug).await?;
    Ok(Json(ProductDto::from(product)))
}

Path parse fail giờ trả AppError::BadRequest với envelope chuẩn:

{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Invalid URL: Cannot parse `abc` to a `i64`",
    "details": null
  }
}

Format này nhất quán với mọi error response khác của Shop API. Tương tự pattern cho AppQuery<T> impl FromRequestParts, AppJson<T> impl FromRequest với map rejection sang AppError::Validation (kèm field-level errors từ validator crate sẽ wire ở B41). Folder crates/shop-api/src/extractors/ đã có placeholder từ B17 — B32 sẽ tạo 3 file path.rs, query.rs, json.rs + mod.rs re-export.

8

State<T> Là Extractor Đặc Biệt

State<S> trong axum đứng riêng so với các extractor khác về bản chất: nó KHÔNG thực sự "parse" request, chỉ clone giá trị state đã .with_state(state) lock vào Router (lock B28). Implementation conceptual:

// axum/src/extract/state.rs (rút gọn)
pub struct State<S>(pub S);

impl<OuterState, InnerState> FromRequestParts<OuterState> for State<InnerState>
where
    InnerState: FromRef<OuterState>,
    OuterState: Send + Sync,
{
    type Rejection = Infallible;

    async fn from_request_parts(
        _parts: &mut Parts,
        state: &OuterState,
    ) -> Result<Self, Infallible> {
        let inner = InnerState::from_ref(state);
        Ok(State(inner))
    }
}

2 điểm cần chú ý:

  • type Rejection = Infallible — kiểu std::convert::Infallible là enum không có variant nào, không thể construct giá trị nào của type này. Đây là cách Rust biểu diễn "function không thể fail". State luôn return Ok, không bao giờ Err.
  • Compile-time check — nếu Router không lock state đúng kiểu (vd handler require State<AppState> mà Router gọi .with_state(())), trait bound InnerState: FromRef<OuterState> không thỏa, compile error. Tức rejection của State effectively unreachable trong code đã build — sai thiết lập state là compile error chứ không runtime.

Pattern handler dùng State (đã thấy B28):

// File: crates/shop-api/src/routes/products.rs (sau B28)
async fn list_products(
    State(state): State<AppState>,         // ← clone state, không bao giờ fail
    Query(pagination): Query<Pagination>,  // ← có thể fail (rejection)
) -> Json<ListResponse<Value>> {
    tracing::info!(port = state.config.port, "listing products");
    ...
}

State không thể fail, nó luôn được đặt đầu arg list theo convention Shop API — không cần lo về thứ tự ảnh hưởng error response. Ngay cả khi đảo State xuống cuối (trừ body), behavior runtime không đổi; nhưng convention đặt đầu để giữ đồng bộ với mọi handler khác (lock B28).

Generic FromRef<OuterState> cho phép extract sub-state — vd State<PgPool> từ State<AppState { pool, redis, ... }> nếu impl FromRef<AppState> for PgPool. Pattern này hữu ích khi handler chỉ cần 1 field của AppState. Shop API hiện tại không tách sub-state (handler luôn nhận State<AppState> đủ resource), có thể bật khi muốn type narrowing chặt hơn ở G6+.

9

Tổng Kết

  • 2 trait cốt lõi axum::extract: FromRequestParts<S> (no body, dùng cho Path/Query/State/TypedHeader/HeaderMap/Extension) và FromRequest<S, B> (consume body, dùng cho Json/Form/Bytes/String/Multipart).
  • Body extractor (Json/Form/Bytes/Multipart) PHẢI đặt cuối arg list — body là single-pass stream, consume xong là hết; đảo order compile error "the trait FromRequest is not implemented for X".
  • Execution order: tuần tự từ trái sang phải, await riêng mỗi extractor, fail nhanh khi 1 extractor return rejection STOP không chạy tiếp; arg đã extract trước bị discard khi extractor sau fail.
  • Convention Shop API lock vĩnh viễn (B22 confirm): State → Path → Query → TypedHeader/HeaderMap → Extension → body — mọi handler từ G7 onward tuân thủ.
  • Mỗi extractor có type Rejection: IntoResponse — default Json fail trả 422, Path/Query fail trả 400 với generic message text/plain.
  • Shop API custom wrapper extractor AppPath<T>, AppQuery<T>, AppJson<T> → rejection map sang AppError::BadRequest/AppError::Validation với envelope chuẩn (lock B16) — implement đầy đủ B32 trong folder crates/shop-api/src/extractors/ (placeholder ready B17).
  • State<S> extractor đặc biệt — clone state đã .with_state() set vào Router, type Rejection = Infallible, không thực sự fail; sai thiết lập state là compile error chứ không runtime.
  • Foundation cho B32-B40 implement custom extractor cụ thể (CurrentUser, TypedHeader, Cookie, Form, Multipart, Bytes, Streaming, Extension, ResponseBuilder).
10

Bài Tập Củng Cố

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

  1. Phân biệt FromRequestParts<S>FromRequest<S, B>. Khi nào dùng trait nào? Liệt kê 3 built-in extractor cho mỗi trait.
  2. Tại sao chỉ 1 body extractor được phép trong handler? Order matters thế nào? Mô tả ngắn flow axum extract từ request đến handler.
  3. Anti-pattern Json<Body> trước Path<u64> trong arg list — compile error tiếng Anh của rustc nói gì? Tại sao compile-time chứ không runtime?
  4. Default rejection của Json<T> parse fail trả status code gì với format gì? Shop API muốn override thành gì? Pattern tên 3 wrapper extractor sẽ implement ở B32.
  5. State<T> có thực sự "fail extract" không? type Rejection của nó là gì? Khi nào rejection của State unreachable trong code đã build?
Đáp án
  1. FromRequestParts<S> extract từ parts của request (method, URI, headers, path params, query string, extensions map) KHÔNG consume body — method nhận &mut Parts mutable reference (cho phép sửa headers nếu cần) cùng &S state lock vào Router, async return Result<Self, Self::Rejection>. Handler có thể có nhiều extractor loại này cùng lúc vì parts vẫn còn cho extractor sau. Dùng khi: extract path param (Path<T>), query string (Query<T>), state shared (State<S>), typed header (TypedHeader<H>), raw header map (HeaderMap), giá trị middleware inject (Extension<T>). FromRequest<S, B> extract TOÀN BỘ request bao gồm body — method nhận Request<B> owned (consume cả parts lẫn body), không trả lại được. Vì body là single-pass stream nên CHỈ 1 extractor loại này trong handler. Dùng khi: deserialize body application/json (Json<T>), form urlencoded (Form<T>), body raw thành Bytes hoặc String, stream multipart/form-data (Multipart). axum cung cấp blanket impl: T: FromRequestParts<S> tự động T: FromRequest<S, B> theo chiều một — cho phép Path/Query/State đứng cạnh Json trong cùng handler; chiều ngược lại không có vì body extractor cần consume body không thể work như part extractor.
  2. Chỉ 1 body extractor được phép vì body HTTP là single-pass stream — đọc xong là hết, không seek lại. FromRequest<S, B> nhận Request<B> owned consume toàn bộ, sau gọi không còn body cho extractor sau. Order matters cứng: body extractor PHẢI ở vị trí CUỐI arg list, compiler enforce qua trait bound của Handler<T, S> — mọi arg trừ arg cuối phải impl FromRequestParts, arg cuối có thể impl một trong hai. Flow axum extract chi tiết: (a) axum nhận Request incoming, split thành Parts (method/URI/headers/extensions) và Body stream; (b) loop arg list trái sang phải, mỗi arg FromRequestParts axum await from_request_parts(&mut parts, &state) riêng — fail thì STOP return rejection.into_response() ngay cho client (HTTP error response), không chạy extractor tiếp; (c) arg cuối nếu là body extractor (FromRequest), axum reconstruct Request từ parts + body rồi await from_request(req, &state) — consume cả hai; (d) chỉ khi mọi extractor đều Ok, axum gọi handler với toàn bộ arg đã extract; (e) handler return implement IntoResponse được axum gọi .into_response() ghi socket trả client. Hệ quả model fail-fast: arg đã extract trước bị discard khi extractor sau fail (vì nằm trong stack frame, function early-return), nhưng đây là chi phí chấp nhận được — parts extract cheap (parse chuỗi vài byte), chi phí thật chỉ ở Json body deserialize.
  3. Compile error message điển hình của rustc: "the trait bound fn(Json<Body>, Path<u64>) -> ... {handler}: Handler<_, _> is not satisfied" kèm help "the trait Handler<T, S> is implemented for fns that take extractors with FromRequest as the LAST argument" và note "the trait FromRequestParts is not implemented for Json<Body>". Nguyên nhân: Json<T> chỉ impl FromRequest (consume body), KHÔNG impl FromRequestParts vì không thể extract Json mà không đụng body. Trait bound của Handler trong axum tổ chức theo pattern "mọi arg trừ arg cuối phải impl FromRequestParts, arg cuối impl FromRequest hoặc FromRequestParts". Đặt Json không phải cuối vi phạm bound này vì rustc cố tìm impl FromRequestParts<S> for Json<Body> không thấy. Tại sao compile-time chứ không runtime: Rust trait system static dispatch + monomorphization — mọi trait bound được check ở compile time qua type checker, function signature handler được axum thông qua macro generic get(handler) / put(handler) ép kiểu vào impl Handler<T, S> tại site .route(...); nếu type không thỏa bound, compile error ngay. Ưu điểm so với runtime check (Express/Spring style): bug order arg không bao giờ slip vào production, dev biết ngay khi cargo build, không cần test runtime để phát hiện; cost không có vì check happen 1 lần ở build time, runtime hoàn toàn không có overhead. Lý do framework dynamic language (Express, Django, Spring) phải runtime check vì không có trait system tĩnh — đổi lại linh hoạt cho meta-programming nhưng đẩy bug runtime.
  4. Default rejection của Json<T> parse fail trả status code 422 Unprocessable Entity (cho body parse fail) hoặc 415 Unsupported Media Type (cho content-type không phải application/json), format text/plain raw với body message generic vd "Failed to deserialize the JSON body into the target type: missing field `name` at line 3 column 5" hoặc "Expected request with `Content-Type: application/json`". Default này functional cho prototype nhưng Shop API muốn override thành AppError::BadRequest hoặc AppError::Validation (lock B16) trả envelope chuẩn {"error": {"code": "BAD_REQUEST", "message": "...", "details": null}} hoặc {"error": {"code": "VALIDATION_ERROR", "message": "...", "details": {"field_errors": {...}}}} với content-type application/json — nhất quán với mọi error response khác của Shop API (success Json envelope {data, meta}, error Json envelope {error}). 2 lý do override: (a) format response nhất quán — client chỉ xử lý 1 format Json envelope cho cả success và error; (b) giữ context envelope chuẩn — error có code machine-readable client switch logic, message human-readable cho UI, details optional field-level errors cho form validation. Pattern tên 3 wrapper extractor implement ở B32: AppPath<T> wrap Path<T> impl FromRequestParts map rejection → AppError::BadRequest (file crates/shop-api/src/extractors/path.rs); AppQuery<T> wrap Query<T> impl FromRequestParts map rejection → AppError::BadRequest (file extractors/query.rs); AppJson<T> wrap Json<T> impl FromRequest map rejection → AppError::Validation kèm field-level errors từ validator crate sẽ wire ở B41 (file extractors/json.rs). Re-export qua extractors/mod.rs để handler import use crate::extractors::{AppPath, AppQuery, AppJson};.
  5. Không, State<T> KHÔNG thực sự "fail extract" trong runtime. Implementation conceptual: type Rejection = std::convert::InfallibleInfallible là enum không có variant nào trong stdlib (pub enum Infallible {}), không thể construct giá trị nào của type này. Đây là cách Rust biểu diễn "function không thể fail" ở type system level. Method from_request_parts của State<S> luôn return Ok(State(state.clone())) — chỉ clone giá trị state đã .with_state(state) lock vào Router (lock B28), không "parse" gì cả. Vì Infallible không construct được, rejection của State không bao giờ có giá trị thực — branch Err(_) trong match xử lý kết quả luôn unreachable. Rejection của State unreachable trong code đã build khi: (a) Router lock state đúng kiểu qua .with_state(AppState { ... }) ở cuối build chain (lock B28); (b) handler require State<AppState> match với kiểu state Router; (c) trait bound InnerState: FromRef<OuterState> thỏa — nếu handler require sub-state State<PgPool> phải impl FromRef<AppState> for PgPool. Nếu sai thiết lập state (vd quên .with_state(), hoặc .with_state(()) mà handler require State<AppState>) thì compile error chứ KHÔNG runtime — rustc reject .route("/products", get(handler)) vì trait bound Handler<T, AppState> for fn(State<AppState>) -> ... không thỏa với Router type Router<()>. Compile error message điển hình: "the trait Handler<(State<AppState>,), ()> is not implemented for ..." hoặc "the trait bound AppState: FromRef<()> is not satisfied". Hệ quả: dev không bao giờ thấy State extract fail trong production logs, vì bug đã chặn ở compile time. Đây là một ví dụ của type-driven correctness — Rust ép sai thiết lập infrastructure ra compile error chứ không runtime bug, giảm class lỗi cả một chiều.
11

Bài Tiếp Theo

— implement đầy đủ AppPath<T>, AppQuery<T>, AppJson<T> wrapper trong crates/shop-api/src/extractors/, rejection map sang AppError::BadRequest/AppError::Validation với envelope chuẩn (lock từ B22/B23/B31).