Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu error message default của
serde_json— có line + column nhưng KHÔNG có path field. - Phân biệt 4 loại error parse JSON qua
serde_json::error::Category:Io,Syntax,Data,Eof. - Áp dụng crate
serde_path_to_errorđể có path field đầy đủ nhưorder.items[2].quantity. - Customize error envelope theo Stripe-style (
param,message,code) — so sánh với flat envelope Shop API. - Refactor
AppJsonextractor (lock B32) Shop API trả 2 lỗi structured:JsonSyntaxvới line+column +JsonDataMismatchvới path. - Hiểu trade-off security khi expose internal struct path qua error response — pros UX dev vs cons lộ schema attacker.
serde_json Default Error — Line + Column
Khi parse JSON fail, serde_json::Error có sẵn 2 thông tin định vị byte trong text body: line (số dòng, 1-based) và column (cột, 1-based). Truy cập qua method error.line() + error.column():
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateProductDto {
name: String,
price: u64,
}
fn main() {
// JSON thiếu value sau "price":
let json = r#"{"name": "x", "price": }"#;
let err = serde_json::from_str::<CreateProductDto>(json).unwrap_err();
println!("{}", err);
// expected value at line 1 column 24
println!("line={} column={}", err.line(), err.column());
// line=1 column=24
}
3 method chính của serde_json::Error:
error.line() -> usize— dòng vị trí lỗi (1-based, 0 nếu unknown).error.column() -> usize— cột vị trí lỗi (1-based, 0 nếu unknown).error.classify() -> Category— phân loại nguyên nhân lỗi qua enum 4 variant (bước 3 sẽ deep).
Pitfall không có path field: với JSON nested deep như {"order": {"items": [{"id": 1, "quantity": "abc"}]}}, error trả về chỉ có "expected u32 at line 1 column 47" — client UI biết byte 47 fail nhưng KHÔNG biết field nào trong struct Rust fail. Dev frontend phải đếm tay byte trong body, map ngược về vị trí field order.items[0].quantity mới highlight được UI input.
Use case cần path field: form admin tạo product với nested categories + variants + metadata, request fail thì UI cần highlight ĐÚNG input field người dùng vừa nhập sai. Báo cáo error chung chung "JSON invalid" không đủ — user phải tự dò 30 field form. Bước 4 sẽ giới thiệu serde_path_to_error crate giải quyết.
4 Loại serde_json::error::Category
Enum serde_json::error::Category phân loại nguyên nhân parse fail thành 4 variant, mỗi loại cần status code HTTP khác nhau:
Io— lỗi đọc input (network drop, file read fail). Server lỗi đọc body, KHÔNG phải client xấu → gửi 500 Internal Server Error.Syntax— JSON cú pháp sai (missing comma, bracket, dấu nháy). Client xấu → 400 Bad Request.Data— JSON hợp lệ syntax nhưng không match struct Rust (mismatch type, missing field required). Có thể gửi 400 (parse fail) hoặc 422 (business rule fail) tùy semantic API.Eof— input cụt giữa chừng ({"x":rồi end stream). Client gửi body không complete → 400 Bad Request.
Phân loại qua match err.classify():
use serde_json::error::Category;
use shop_common::error::AppError;
fn map_serde_json_error(err: serde_json::Error) -> AppError {
match err.classify() {
Category::Io => AppError::Internal(anyhow::anyhow!(
"IO error reading body: {}", err
)),
Category::Syntax => AppError::BadRequest(format!(
"JSON syntax error: {}", err
)),
Category::Data => AppError::BadRequest(format!(
"JSON data mismatch: {}", err
)),
Category::Eof => AppError::BadRequest(
"JSON cut off mid-parse".into()
),
}
}
Quyết định Shop API map Data → 400 thay 422: lock B3 quy định 422 là "business rule fail" (validate fail sau parse — vd email format sai, slug trùng). Data mismatch type là "parse fail" (client gửi "abc" cho field type u32) → semantic giống Syntax hơn → 400 BAD_REQUEST. Lock B41 ValidationFailed mới là 422 envelope với fields object.
Bước 5 sẽ refactor AppJson extractor map gộp Io | Syntax | Eof → JsonSyntax variant mới (chứa line+column), riêng Data → JsonDataMismatch variant mới (chứa path) — cho client UI 2 kiểu detail khác nhau.
serde_path_to_error — Path Đầy Đủ
Crate serde_path_to_error (dtolnay maintain, version 0.1.x stable) wrap deserializer track path khi traverse struct nested. Khi gặp lỗi tại field deep, trả về Error chứa path đầy đủ + inner serde_json error gốc.
Thêm dependency vào workspace root Cargo.toml:
# shop/Cargo.toml — [workspace.dependencies]
[workspace.dependencies]
# ... existing deps
serde_path_to_error = "0.1"
Consume ở crates/shop-api/Cargo.toml:
# crates/shop-api/Cargo.toml
[dependencies]
# ... existing
serde_path_to_error = { workspace = true }
Cách dùng cơ bản — wrap serde_json::Deserializer:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Order {
items: Vec<Item>,
}
#[derive(Deserialize, Debug)]
struct Item {
id: u64,
quantity: u32,
}
fn main() {
// items[0].quantity sai type — gửi string "abc" cho u32
let json = r#"{"items": [{"id": 1, "quantity": "abc"}]}"#;
let de = &mut serde_json::Deserializer::from_str(json);
let result: Result<Order, _> = serde_path_to_error::deserialize(de);
match result {
Ok(order) => println!("ok: {:?}", order),
Err(err) => {
println!("path: {}", err.path());
// path: items[0].quantity
println!("inner: {}", err.inner());
// inner: invalid type: string "abc", expected u32 at line 1 column 38
}
}
}
Output format err.path() qua Display trait hỗ trợ 4 trường hợp:
Trường hợp | Format
Field top-level | name
Nested field | order.customer.email
Array index | items[2]
Map key | metadata["warranty_months"]
Combined deep | order.items[0].metadata["color"]
Path đủ thông tin cho client UI highlight đúng field. err.inner() trả về &serde_json::Error gốc — có thể gọi .classify(), .line(), .column() như bước 2-3.
Refactor AppJson Trả Path Detail
Refactor extractor AppJson<T> (lock B32) — thay logic delegate axum::Json bằng pattern đọc Bytes body raw rồi parse qua serde_path_to_error. Map error theo Category: Io | Syntax | Eof gộp thành JsonSyntax (line+column), Data riêng thành JsonDataMismatch (path).
// File: crates/shop-api/src/extractors/json.rs (refactor B48)
use axum::{
body::Bytes,
extract::{FromRequest, Request},
http::{header, HeaderMap},
};
use serde::de::DeserializeOwned;
use serde_json::error::Category;
use shop_common::error::AppError;
pub struct AppJson<T>(pub T);
impl<T, S> FromRequest<S> for AppJson<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = AppError;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
// 1. Validate Content-Type
if !is_json_content_type(req.headers()) {
return Err(AppError::BadRequest(
"Content-Type phải là application/json".into(),
));
}
// 2. Extract bytes (size limit đã handle bởi DefaultBodyLimit layer B32)
let bytes = Bytes::from_request(req, state)
.await
.map_err(|_| AppError::BadRequest("không đọc được body".into()))?;
// 3. Deserialize qua serde_path_to_error
let de = &mut serde_json::Deserializer::from_slice(&bytes);
let value: T = serde_path_to_error::deserialize(de).map_err(|e| {
let path = e.path().to_string();
let inner = e.inner();
match inner.classify() {
Category::Io | Category::Syntax | Category::Eof => {
AppError::JsonSyntax {
message: inner.to_string(),
line: inner.line(),
column: inner.column(),
}
}
Category::Data => AppError::JsonDataMismatch {
path,
message: inner.to_string(),
},
}
})?;
Ok(AppJson(value))
}
}
fn is_json_content_type(headers: &HeaderMap) -> bool {
headers
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|s| s.contains("application/json"))
.unwrap_or(false)
}
3 thay đổi chính so với B32:
- KHÔNG delegate
axum::Json::from_requestnữa — đọcBytesraw rồi parse trực tiếp quaserde_path_to_error. - Trả 2 variant
AppErrorstructured mới (JsonSyntax+JsonDataMismatch) thay 1 string verbose như B32. - Path field deep nested (
order.items[0].quantity) có sẵn cho client UI highlight đúng field.
Pattern lock B48: mọi custom JSON extractor Shop API tương lai (NDJSON B49, Multipart JSON part B36) MANDATORY dùng serde_path_to_error wrap deserializer — KHÔNG dùng serde_json::from_slice direct vì mất path info.
Thêm 2 AppError Variant Mới
Extend enum AppError trong crates/shop-common/src/error.rs thêm 2 variant — bump tổng số variant từ 12 (B41 lock) lên 14:
// File: crates/shop-common/src/error.rs (extend B48)
#[derive(Debug, thiserror::Error)]
pub enum AppError {
// ... 12 variants existing (B41)
#[error("JSON syntax error at line {line} column {column}: {message}")]
JsonSyntax {
message: String,
line: usize,
column: usize,
},
#[error("JSON data mismatch at {path}: {message}")]
JsonDataMismatch {
path: String,
message: String,
},
}
Update impl IntoResponse for AppError thêm 2 match arm build envelope top-level flat lock B16 + nested detail object chứa structured info riêng:
// File: crates/shop-common/src/error.rs (extend impl IntoResponse)
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
impl IntoResponse for AppError {
fn into_response(self) -> Response {
match &self {
// ... 12 arm existing
AppError::JsonSyntax { message, line, column } => {
let body = json!({
"error": "JSON syntax error",
"code": "JSON_SYNTAX",
"request_id": null,
"detail": {
"line": line,
"column": column,
"message": message,
}
});
(StatusCode::BAD_REQUEST, Json(body)).into_response()
}
AppError::JsonDataMismatch { path, message } => {
let body = json!({
"error": "JSON data mismatch",
"code": "JSON_DATA_MISMATCH",
"request_id": null,
"detail": {
"path": path,
"message": message,
}
});
(StatusCode::BAD_REQUEST, Json(body)).into_response()
}
}
}
}
2 envelope JSON wire format:
// JsonSyntax variant — 400 Bad Request
{
"error": "JSON syntax error",
"code": "JSON_SYNTAX",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"detail": {
"line": 1,
"column": 24,
"message": "expected value at line 1 column 24"
}
}
// JsonDataMismatch variant — 400 Bad Request
{
"error": "JSON data mismatch",
"code": "JSON_DATA_MISMATCH",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"detail": {
"path": "items[0].quantity",
"message": "invalid type: string \"abc\", expected u32 at line 1 column 38"
}
}
Field request_id placeholder null tạm thời — middleware B39 enrich_error_response đã wire để inject request_id thật qua re-parse JSON body trước khi gửi response. Mọi error envelope Shop API consistency có request_id cho correlation log/tracing.
Stripe-Style Error Envelope
Stripe API error envelope wrap mọi field bên trong object error top-level — pattern industry phổ biến:
// Stripe-style nested
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"param": "amount",
"message": "amount must be a positive integer",
"doc_url": "https://stripe.com/docs/error-codes/parameter-invalid"
}
}
So sánh với Shop API flat envelope lock B16 từ đầu series:
// Shop API flat top-level
{
"error": "amount must be a positive integer",
"code": "VALIDATION_FAILED",
"request_id": "550e8400-...",
"fields": {
"amount": ["amount phải lớn hơn 0"]
}
}
2 trade-off chính khi chọn:
- Stripe nested — wrap mọi field trong 1 namespace
error, dễ extend field metadata (doc_url,request_log_url,charge_id) không phá level top. - Flat top-level (Shop API) — đơn giản hơn cho client parse, consistent với success response (
{data, meta}sẽ là{error, code, request_id}cùng level), match pattern envelope đã lock B41ValidationFailedvớifieldsobject.
Lock decision Shop API GIỮ flat envelope vì 3 lý do:
- Consistency với envelope
ValidationFailed422 lock B41 (fieldsobject top-level cùngerror+code+request_id) — refactor sang Stripe nested phải đổi B41 + B16 cùng lúc, breaking change. - Client parse 1 level đơn giản hơn —
response.codethayresponse.error.code, ít typo, error message dev đọc log trace dễ hơn. - Field metadata extend qua nested
detailobject per variant — flexibility vẫn đủ, không cần wrap nested top-level.
Pattern envelope đầy đủ lock B48 vĩnh viễn:
Top-level (MANDATORY mọi error):
error human message (string) — Display trait từ thiserror
code machine SCREAMING_SNAKE (string) — "JSON_SYNTAX", "VALIDATION_FAILED"
request_id correlation UUID (string|null) — enrich qua middleware B39
Nested object (optional theo variant):
detail structured info riêng từng error type
- JsonSyntax: line + column + message
- JsonDataMismatch: path + message
- future: variant tự define field
fields field-name → messages map (cho ValidationFailed 422 lock B41)
Variant nào cần structured info thêm thì add object con tương ứng (detail cho JSON parse, fields cho ValidationFailed, future retry cho RateLimited extend dynamic). Pattern này extend dễ, không phá envelope existing.
Verify + Trade-Off Security
Verify end-to-end với 3 case curl:
cargo run -p shop-api
# Output: shop-api listening addr=0.0.0.0:3000
Test 1: JSON syntax fail (thiếu value sau "price"):
curl -i -X POST http://localhost:3000/api/v1/products \
-H 'Content-Type: application/json' \
-d '{"name": "x", "price": }'
# HTTP/1.1 400 Bad Request
# content-type: application/json; charset=utf-8
# x-request-id: 550e8400-e29b-41d4-a716-446655440000
#
# {
# "error": "JSON syntax error",
# "code": "JSON_SYNTAX",
# "request_id": "550e8400-e29b-41d4-a716-446655440000",
# "detail": {
# "line": 1,
# "column": 24,
# "message": "expected value at line 1 column 24"
# }
# }
Test 2: JSON data mismatch (type sai trong nested struct — gửi string cho field price: Money):
curl -i -X POST http://localhost:3000/api/v1/products \
-H 'Content-Type: application/json' \
-d '{"name": "x", "slug": "x-slug", "price": "abc", "stock": 1}'
# HTTP/1.1 400 Bad Request
# content-type: application/json; charset=utf-8
#
# {
# "error": "JSON data mismatch",
# "code": "JSON_DATA_MISMATCH",
# "request_id": "550e8400-...",
# "detail": {
# "path": "price",
# "message": "invalid type: string \"abc\", expected ..."
# }
# }
Test 3: Nested array deep (bulk import items với field sai type ở index 2):
curl -i -X POST http://localhost:3000/api/v1/admin/products/import \
-H 'Content-Type: application/json' \
-d '{"items": [{"name":"a","price":100},{"name":"b","price":200},{"name":"c","price":"xyz"}]}'
# HTTP/1.1 400 Bad Request
#
# {
# "error": "JSON data mismatch",
# "code": "JSON_DATA_MISMATCH",
# "request_id": "...",
# "detail": {
# "path": "items[2].price",
# "message": "invalid type: string \"xyz\", expected u64 ..."
# }
# }
Path items[2].price chỉ đúng field nested deep → client UI parse path tách items + index 2 + field price → highlight đúng row trong bulk form.
Trade-off security expose path field:
- Pros: client dev biết chính xác field name nào fail trong struct deep → fix UI highlight nhanh, không phải đoán; reduce thời gian debug request fail từ 30 phút (đếm tay byte) xuống 1 phút (đọc path).
- Cons: lộ internal struct field name của Rust DTO ra wire → attacker enumeration đoán schema (User có roles, Order có items[].product_id, Cart có discount_code), guess relationship entity → tăng attack surface; nếu schema có field nhạy cảm (vd
internal_audit_flag,kyc_status) — lộ qua error.
Mitigation env flag: thêm field expose_error_detail: bool vào AppConfig (B10) — default true dev/staging, false production. Khi false, IntoResponse chỉ trả top-level error + code + request_id, ẩn detail object. Debug-ability giữ qua server-side log full chi tiết trong tracing::error! với request_id correlation — dev nội bộ tra log theo request_id thấy detail, client/attacker bên ngoài chỉ thấy generic message.
Lock decision Shop API: giữ detail trong dev/staging cho UX dev nhanh, disable trong production qua env config AppConfig.expose_error_detail = false default. Group 19 (Observability) sẽ deep implement env-driven config + log correlation pattern.
Tổng Kết
serde_jsondefault error: có sẵn line + column quaerror.line()+error.column(), KHÔNG có path field nên client UI không biết field nào fail.- 4
Category:Io(500),Syntax(400),Data(400 hoặc 422 tùy semantic — Shop API chọn 400),Eof(400). Phân loại để gửi status code phù hợp. - Crate
serde_path_to_error: wrap deserializer track path đầy đủ —field,parent.child,items[2], combinedorder.items[0].metadata["key"]. - 2
AppErrorvariant mới:JsonSyntax { message, line, column }(variant 13) +JsonDataMismatch { path, message }(variant 14), bump tổng từ 12 → 14. - Stripe-style error envelope nested wrap object
errortop-level — so sánh với Shop API flat lock B16. - Lock decision Shop API GIỮ flat envelope: consistency với
ValidationFailed422fieldsobject B41 + client parse 1 level đơn giản + field metadata extend quadetailnested object. - Pattern envelope đầy đủ lock B48: top-level 3 field MANDATORY (
error,code,request_id) + nesteddetailobject cho JsonSyntax/JsonDataMismatch +fieldscho ValidationFailed. AppJsonrefactor: thay delegateaxum::Jsonbằng đọcBytesraw + parse quaserde_path_to_error::deserialize+ matchCategory4 variant gộpIo|Syntax|Eof→JsonSyntax,Data→JsonDataMismatch.- Trade-off security: expose path field UI-friendly cho dev fix nhanh vs lộ internal schema attacker → mitigation env flag
AppConfig.expose_error_detaildefault dev true production false. request_idenrich qua middleware B39 (consistent với mọi error variant — top-level field).- File path lock:
crates/shop-api/src/extractors/json.rsrefactor;crates/shop-common/src/error.rs2 variant mới + 2 match arm IntoResponse. - Workspace deps mới:
serde_path_to_error = "0.1"thêm[workspace.dependencies]+ consume quashop-api/Cargo.toml. - Foundation cho B49 (NDJSON parse error multi-line — line-by-line error report), B50 (compression decode error category — gzip/brotli decompress fail trước serde parse).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
serde_jsondefault error có thông tin gì khi parse fail? Tại sao KHÔNG có path field? Use case nào cần path field?- 4
Categorylà gì? Mỗi loại nên trả status code HTTP nào và lý do? Shop API mapDatasang 400 hay 422, vì sao? serde_path_to_erroradd gì cho error message? Cho ví dụ path nested với array index và map key.- Stripe-style error envelope có structure gì? So sánh với Shop API flat envelope — 2 trade-off chính và lý do Shop API GIỮ flat.
- Trade-off expose path field qua error message — pros và cons? Mitigation nào áp dụng cho production và làm sao dev vẫn debug được?
Đáp án
serde_jsondefault error thông tin có sẵn: 2 thông tin định vị byte trong text body —error.line() -> usizedòng (1-based) vàerror.column() -> usizecột (1-based) +error.classify() -> Categoryphân loại nguyên nhân fail. Vd parser#"{"name": "x", "price": }"#fail thiếu value sau "price" →error.line() == 1,error.column() == 24. Tại sao KHÔNG có path field:serde_json::Errorđược build trongserde_json::Deserializerinternal — deserializer parse từng token JSON sequential KHÔNG track ngữ cảnh "đang ở field nào của struct Rust" vìserdeframework design tách deserializer (format-agnostic) và visitor (type-aware). Deserializer chỉ biết "đang ở byte vị trí X" chứ không biết "field X thuộcorder.items[0].quantity". Path field cần track qua wrapper crate riêng —serde_path_to_errorbước 4. Use case cần path field: (a) form admin Shop API tạo product với 30 field nested + categories + variants + metadata → request fail thì UI cần highlight đúng input field người vừa nhập sai, báo cáo "JSON invalid line 5 column 17" không đủ — user phải tự dò 30 field; (b) bulk import endpoint admin (POST /api/v1/admin/products/importvới array 1000 items) — fail tạiitems[847].pricesai type, client cần biết row 847 fail thay đếm tay 850 row JSON; (c) webhook integration third-party (Stripe, GitHub) gửi nested payload phức tạp — debug payload fail phải biết path để check spec docs đúng field; (d) GraphQL/JSON Schema client auto-generate form từ schema — fail map ngược path field UI library highlight tự động.- 4
Category+ status code mapping. (a)Io: lỗi đọc input — network drop khi đọc body từ socket, file read fail nếu parse từ disk (vd CSV import streaming). Lỗi phía server (kết nối/IO), KHÔNG phải client xấu → gửi 500 Internal Server Error + logtracing::error!để dev biết investigate. Hiếm gặp ở HTTP handler vì tower-http buffer body sẵn — chủ yếu xuất hiện ở CLI tool đọc file lớn. (b)Syntax: JSON cú pháp sai — missing comma{"a":1 "b":2}, missing bracket{"items": [1, 2, dấu nháy lệch{'name': 'x'}(JSON yêu cầu double-quote không single-quote), trailing comma{"a":1,}. Client gửi body invalid → gửi 400 Bad Request. (c)Data: JSON hợp lệ syntax nhưng KHÔNG match struct Rust — type mismatch (gửi"abc"cho fieldprice: u32), missing field required (struct requirenamenhưng body không có), unknown variant enum (gửi"unknown"cho fieldstatus: OrderStatuschỉ accept "pending"/"paid"/"shipped"). Có thể 400 hoặc 422 tùy semantic API. (d)Eof: input cụt giữa chừng —{"x":rồi end stream (connection drop, body truncate, file read EOF sớm). Client gửi body không complete → gửi 400 Bad Request. Shop API mapData→ 400 thay 422 vì 3 lý do: (i) lock B3 quy định 422 là "business rule fail" (validate fail sau parse — email format sai, slug trùng, stock dưới 0 vi phạm business rule).Datamismatch type là "parse fail" — client gửi"abc"cho fieldu32không phải vi phạm business rule, mà gửi sai schema; (ii) consistency vớiSyntax+Eofcũng 400 — cả 3 đều là "client gửi body invalid không thể parse vào struct Rust"; (iii) lock B41ValidationFailedmới là 422 envelope vớifieldsobject (validator crate chạy SAU khi parse thành công). Nếu mapData→ 422 sẽ chồng chéo semantic vớiValidationFailed— client confuse 2 case dùng cùng status code 422 nhưng envelope khác (1 códetail, 1 cófields). serde_path_to_erroradd gì: crate wrap deserializer track path field đầy đủ khi traverse struct nested. Khi gặp lỗi tại field deep, trả vềErrorchứa: (a)err.path()trả&PathimplDisplaytrait — output format string path đầy đủ; (b)err.inner()trả&serde_json::Errorgốc — có thể call.classify(),.line(),.column()như default. Wire usage: thayserde_json::from_slice(&bytes)bằng pattern wrap deserializer:
Formatlet de = &mut serde_json::Deserializer::from_slice(&bytes); let value: T = serde_path_to_error::deserialize(de)?;err.path()output 4 trường hợp: (i) field top-level:name(chỉ tên field); (ii) nested field:order.customer.email(dot-separator giữa parent.child); (iii) array index:items[2](bracket index 0-based); (iv) map key:metadata["warranty_months"](bracket + double-quote key string); (v) combined deep:order.items[0].variants[1].metadata["color"]kết hợp đủ 4 case. Ví dụ nested array + map key:
Path đủ thông tin cho client UI parse tách: array name#[derive(Deserialize)] struct Order { items: Vec<OrderItem>, } #[derive(Deserialize)] struct OrderItem { id: u64, metadata: HashMap<String, serde_json::Value>, } let json = r#"{"items": [ {"id": 1, "metadata": {"color": "red"}}, {"id": 2, "metadata": {"warranty_months": "abc"}} ]}"#; // Parse fail tại items[1].metadata key "warranty_months" // nếu type metadata expect u32 thay Value // path: items[1].metadata["warranty_months"] // inner: invalid type: string "abc", expected u32 at line 3 column 56items+ index1+ fieldmetadata+ map key"warranty_months"→ highlight ĐÚNG cell trong table form UI. KHÔNG cóserde_path_to_errorchỉ biết "line 3 column 56" — phải đếm tay JSON body.- Stripe-style nested envelope structure:
Mọi field error wrap trong 1 object{ "error": { "type": "invalid_request_error", "code": "parameter_invalid", "param": "amount", "message": "amount must be a positive integer", "doc_url": "https://stripe.com/docs/error-codes/parameter-invalid" } }errortop-level —type(loại error),code(machine code),param(field name fail),message(human-readable),doc_url(link docs Stripe). Shop API flat envelope lock B16:
Top-level 3 field MANDATORY ({ "error": "amount must be a positive integer", "code": "VALIDATION_FAILED", "request_id": "550e8400-...", "fields": { "amount": ["amount phải lớn hơn 0"] } }error,code,request_id) + optional nested object riêng từng variant (fieldscho ValidationFailed 422 lock B41,detailcho JsonSyntax/JsonDataMismatch lock B48). 2 trade-off chính: (i) Stripe nested pros dễ extend metadata (doc_url,request_log_url,charge_idStripe-specific) không phá level top, namespaceerrorchứa tất cả info; cons client phải parse 2 level (response.error.code) thay 1 level, code dài hơn dễ typo, response success/error structure khác nhau (success{data, meta}top-level, error{error: {...}}nested) — client phải biết check status code trước khi parse. (ii) Flat top-level pros đơn giản parse 1 level (response.code), consistent với success response ({data, meta}+{error, code, request_id}cùng pattern top-level), match B41ValidationFailedvớifieldstop-level đã lock; cons ít namespace cho metadata phức tạp — phải dùng nested object con per variant (detail,fields, futureretry) extend dynamic, nếu cần wrap toàn bộ thì phải refactor breaking change. Shop API lock GIỮ flat vì 3 lý do: (a) consistency vớiValidationFailed422fieldsobject B41 — refactor sang Stripe nested phải đổi B41 + B16 + B48 cùng lúc, breaking client production; (b) client parse 1 level đơn giản hơn — đặc biệt VN team mới mid-level dev đọc log debug dễ; (c) field metadata extend đủ qua nested object con per variant (detailcho JSON parse,fieldscho validation, future variant tự define field) — flexibility vẫn đủ, không cần wrap nested top-level. - Trade-off expose path field qua error message. Pros: (i) client dev biết chính xác field name nào fail trong struct deep nested — fix UI highlight nhanh, không phải đoán field; (ii) reduce thời gian debug request fail từ 30 phút (đếm tay byte JSON body) xuống 1 phút (đọc path
items[2].price); (iii) UX user cuối cùng tốt hơn — form admin highlight đúng input người vừa nhập sai, không phải đỏ toàn form chung chung; (iv) bulk import endpoint admin (POST /admin/products/import) báo cáo row nào fail trong batch 1000 items — admin sửa 1 row thay re-upload toàn file; (v) integration test viết dễ hơn — asserterror.detail.path == "items[0].quantity"precise thay grep substring vague. Cons: (i) lộ internal struct field name Rust DTO ra wire → attacker enumeration đoán schema (User có roles + permissions, Order có items[].product_id + shipping.address.country, Cart có discount_code + applied_promo) — guess relationship entity → tăng attack surface mapping; (ii) nếu schema có field nhạy cảm (vdinternal_audit_flag,kyc_status,fraud_scorechỉ admin internal dùng) — lộ qua error khi attacker fuzz body intentionally trigger parse error; (iii) version schema lộ qua field naming convention (v2_pricing_strategy,legacy_payment_method) — attacker biết version migration progress; (iv) competitor reverse-engineer business model qua field structure (vd Stripe schema lộradar_session_id→ competitor đoán fraud detection model). Mitigation env flag áp dụng cho production: thêm fieldexpose_error_detail: boolvàoAppConfig(B10) — defaulttruedev/staging cho UX dev nhanh,falseproduction. Khifalse,IntoResponsematch armJsonSyntax/JsonDataMismatchchỉ trả top-levelerror+code+request_id, ẩndetailobject — client production thấy "JSON syntax error" generic. Dev vẫn debug được qua server-side log:impl IntoResponse for AppErrorluôntracing::error!(error = ?self, request_id = %req_id, "json parse fail")log full chi tiết server-side với request_id correlation. Dev nội bộ tra log theo request_id (client gửi headerX-Request-Idhoặc lấy từ response header) thấy detail full — không cần wire chi tiết về client. Pattern: "client minimal, server verbose" — wire response ngắn gọn an toàn, log server đầy đủ cho debug. Group 19 (Observability) sẽ deep implement env-driven configAppConfig.expose_error_detail+ log correlation pattern vớitracingspan attach request_id + structured field. Lock decision Shop API: giữ detail trong dev/staging cho UX dev, disable production qua env config defaultfalse— dev cần test production-like phải explicit setEXPOSE_ERROR_DETAIL=truetrong staging environment.
Bài Tiếp Theo
Bài 49: NDJSON + Streaming JSON Parse — newline-delimited JSON cho bulk export/import, parse line-by-line không load all to RAM, axum response stream NDJSON, áp products export endpoint Shop API.
