Mục lục
- Mục Tiêu Bài Học
- Vấn Đề: Axum Default 404/405 Response
- Implement not_found Fallback
- Implement method_not_allowed Fallback Với Allow Header
- OPTIONS Handler — Tự Động Qua CORS
- Panic Catch Layer — Last Resort Defense
- Uniform Error Envelope Architecture
- Verify End-To-End + Edge Cases
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Implement
not_foundfallback thay axum default text"Not Found". - Implement
method_not_allowedfallback vớiAllowheader chính xác. - Hiểu OPTIONS handler default axum auto (qua CORS B77).
- Implement panic catch layer với
CatchPanicLayer. - Pattern uniform error envelope cho mọi error path (handler error + fallback + panic).
- Verify edge case: typo route, wrong method, panic handler.
- File path lock: extend
handlers/fallback.rs(refactor từ B19 placeholder), NEWmiddleware/panic_catch.rs. - Stack giờ 11 layer (10 cũ B82 +
panic_catchinnermost).
Vấn Đề: Axum Default 404/405 Response
Mặc định axum trả response thô khi không match route hoặc sai method. Hai trường hợp đặc trưng:
- 404 Not Found: body
Not Founddạngtext/plain, không có header phụ trợ, không có envelope. - 405 Method Not Allowed: empty body với header
Allow: GET, POSTliệt kê method định nghĩa cho route, nhưng nội dung response vẫn rỗng.
Pattern default tiện cho prototype nhưng gây 3 vấn đề khi service đi vào production:
- Client expect JSON envelope (B16 lock
{error, code, request_id, detail?}) → frontend gọiresponse.json()trên body text"Not Found"sẽ throw SyntaxError, message hiển thị sai cho user ("Không thể phân tích phản hồi từ server") thay vì "Không tìm thấy route". - Log aggregator (Loki/Datadog/Honeycomb) parse JSON ingest mọi line. Mix body text với JSON khiến parser fail, query attribute filter (
{code="NOT_FOUND"}) không trả kết quả → blind spot khi debug. request_idKHÔNG có trong response → user báo lỗi qua support chat không gửi kèm trace ID, kỹ sư phải tự đào log theo timestamp ± IP → rất chậm so với search 1 chiềurequest_idduy nhất.
Giải pháp lock cho Shop API: uniform JSON envelope cho mọi error path. Mặt sau của lock này là 3 entry point sinh error (handler trả AppError + fallback 404/405 + panic catch 500) đều produce body cùng shape — client chỉ cần 1 đoạn code parse envelope chung, log aggregator chỉ cần 1 schema, support chat chỉ cần copy request_id để kỹ sư tra ngay.
Implement not_found Fallback
B19 đã wire placeholder handlers::fallback::not_found vào router với body tạm thời. B84 refactor đầy đủ envelope chuẩn:
// File: crates/shop-api/src/handlers/fallback.rs
use axum::{
http::{Method, StatusCode, Uri},
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
pub async fn not_found(method: Method, uri: Uri) -> Response {
tracing::warn!(
method = %method,
uri = %uri,
"route not matched — 404"
);
let body = json!({
"error": "route not found",
"code": "NOT_FOUND",
"request_id": null, // enrich qua middleware B39
"detail": {
"method": method.as_str(),
"uri": uri.to_string(),
"hint": "check spelling or refer to API docs at /api/docs",
}
});
(StatusCode::NOT_FOUND, Json(body)).into_response()
}
Pattern lock 4 điểm:
- Log
WARNKHÔNGERROR: 404 phổ biến do user gõ sai URL, link cũ cached trong Google search, hoặc bot scan path (/admin,/wp-login.php,/.env) — không phải bug server. Đặt levelERRORsẽ flood alert filterlevel="error"trong Grafana panel, ngấm thật bug 5xx vào noise. Pattern lock: 4xx →WARN, 5xx →ERROR(B80 lock continued). - Envelope B16 chuẩn: 3 field bắt buộc
error+code+request_id(null tại điểm này, middleware enrich sau). detail.method+detail.uri: client thấy ngay request gốc mình gửi, đặc biệt hữu ích khi URL có encode (vd%20giữa slug).detail.hintchung — KHÔNG list available routes: nhiều framework auto-suggest route gần đúng ("Did you mean /api/v1/products?") — tiện cho dev nhưng là suggestion attack trong production. Attacker probe random path để map endpoint surface, hint vô tình tiết lộ route nội bộ (/api/v1/admin/audit). OWASP A05 information disclosure khuyến cáo hint chung kiểu "refer to API docs" thay vì gợi ý cụ thể.
Wire trong router (placeholder B19 đã có, B84 chỉ refactor handler body):
// File: crates/shop-api/src/router.rs (đã có từ B19)
use crate::handlers;
pub fn build_router(state: AppState) -> Router {
Router::new()
// ... routes
.fallback(handlers::fallback::not_found)
// ... layers
}
Implement method_not_allowed Fallback Với Allow Header
Khi route /api/v1/products chỉ định nghĩa GET + POST mà client gửi DELETE, axum mặc định trả 405 với Allow: GET, POST header nhưng body rỗng. Shop API muốn body envelope chuẩn — phải tự manual handler:
// File: crates/shop-api/src/handlers/fallback.rs (extend)
use axum::http::{StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde_json::json;
pub async fn method_not_allowed(uri: Uri) -> Response {
tracing::warn!(uri = %uri, "method not allowed — 405");
// axum chưa expose dễ dàng route's allowed methods qua API
// → fallback: hint chung "check API docs"
let body = json!({
"error": "method not allowed for this route",
"code": "METHOD_NOT_ALLOWED",
"request_id": null,
"detail": {
"uri": uri.to_string(),
"hint": "check API docs for allowed methods on this endpoint",
}
});
(StatusCode::METHOD_NOT_ALLOWED, Json(body)).into_response()
}
Wire trong router song song .fallback() (cả 2 đã có từ B19 placeholder):
// File: crates/shop-api/src/router.rs
Router::new()
// ... routes
.fallback(handlers::fallback::not_found)
.method_not_allowed_fallback(handlers::fallback::method_not_allowed)
// ... layers
Tinh ý: axum vẫn tự sinh Allow header (vd Allow: GET, POST) khi route có định nghĩa method khác — không phụ thuộc body handler. Tức là response 405 từ Shop API có cả 2: body JSON envelope (do handler) + Allow header (do framework). Lý do KHÔNG manual list method trong body:
- axum 0.8 chưa expose API public để handler đọc
allowed_methodscủa route đang fallback. Nếu tự duy trì 1 bảngHashMap<path, Vec<Method>>song song với router definition → 2 nguồn truth dễ lệch (thêmPATCHmới quên update bảng → client thấy 405 nóiAllow: GET, POSTnhưng thực tế server đã acceptPATCH). - Pattern lock: tin
Allowheader axum tự generate (đúng 100% theo router definition) + body envelope chỉ hint chung "check API docs". Client tử tế đọcAllowheader trước khi retry; client lười cầnhintdẫn tới tài liệu.
Pattern lock đầy đủ: log WARN + envelope chuẩn + hint chung. Không khác not_found trừ status code + message.
OPTIONS Handler — Tự Động Qua CORS
Method OPTIONS có 2 use case trong HTTP/1.1:
- CORS preflight (use case 99%): browser tự gửi OPTIONS request trước mọi
POST/PUT/PATCH/DELETEcross-origin để hỏi server cho phép không. - RFC 9110 OPTIONS (use case < 1%): client query khả năng route — phần lớn API hiện đại không support do thừa.
Shop API tận dụng CorsLayer B77 đã wire. tower-http CorsLayer khi gặp OPTIONS preflight (request có header Origin + Access-Control-Request-Method) sẽ chặn trước khi đến handler và trả 204 No Content với đầy đủ 4 header CORS:
Access-Control-Allow-Origin: origin cho phép từ allowlist B77.Access-Control-Allow-Methods:GET, POST, PATCH, DELETE, OPTIONStừ config.Access-Control-Allow-Headers:Content-Type, Authorization, X-Request-Id, Idempotency-Key, ...từ config.Access-Control-Max-Age: 3600s cache preflight, browser khỏi hỏi lại trong 1h.
Lock decision Shop API: KHÔNG custom OPTIONS handler. CorsLayer B77 xử lý đủ. Trường hợp special — OPTIONS gửi tới route nằm ngoài CORS scope (vd /internal/admin chỉ allow same-origin nên không qua CorsLayer): request OPTIONS sẽ rơi vào method_not_allowed_fallback nếu route đó không khai báo OPTIONS, hoặc not_found nếu route không tồn tại. Cả 2 đều trả envelope chuẩn theo step 3 + 4.
Code đối với CorsLayer (đã có ở B77, không thay đổi B84):
// File: crates/shop-api/src/middleware/cors.rs (đã có từ B77)
use tower_http::cors::CorsLayer;
use axum::http::{HeaderName, Method};
pub fn cors_layer() -> CorsLayer {
CorsLayer::new()
.allow_origin(allowed_origins()) // B77 lock allowlist
.allow_methods([
Method::GET, Method::POST, Method::PATCH,
Method::DELETE, Method::OPTIONS,
])
.allow_headers([
HeaderName::from_static("content-type"),
HeaderName::from_static("authorization"),
HeaderName::from_static("x-request-id"),
HeaderName::from_static("idempotency-key"),
])
.max_age(std::time::Duration::from_secs(3600))
}
Panic Catch Layer — Last Resort Defense
Rust không có exception nhưng có panic! — khi handler panic (vd arr[100] out-of-bound, unwrap() trên None, .expect() trên config thiếu), tokio runtime catch panic ở task level và kill task đó. Response default trong axum: empty body 500 + connection close không header.
Hậu quả 3 điểm:
- Client thấy empty body → frontend không parse được envelope.
- Connection close → keep-alive bị ngắt, lần request sau phải bắt tay TLS lại (chậm).
- Panic message không vào log nếu chưa có panic hook → kỹ sư mò mẫm reproduce.
Solution: tower_http::catch_panic::CatchPanicLayer. Layer này wrap inner service, dùng std::panic::catch_unwind bắt panic ở runtime, gọi custom handler convert Box<dyn Any + Send + 'static> sang Response.
Tạo file mới crates/shop-api/src/middleware/panic_catch.rs:
// File: crates/shop-api/src/middleware/panic_catch.rs
use std::any::Any;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use tower_http::catch_panic::CatchPanicLayer;
pub fn panic_catch_layer()
-> CatchPanicLayer<fn(Box<dyn Any + Send + 'static>) -> Response>
{
CatchPanicLayer::custom(handle_panic)
}
fn handle_panic(err: Box<dyn Any + Send + 'static>) -> Response {
// Downcast panic payload sang &str hoặc String (2 dạng phổ biến)
let panic_msg = if let Some(s) = err.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = err.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
tracing::error!(panic = %panic_msg, "handler panicked");
// Production: KHÔNG expose panic message (security)
let body = json!({
"error": "internal server error",
"code": "INTERNAL_ERROR",
"request_id": null,
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(body)).into_response()
}
Cập nhật crates/shop-api/src/middleware/mod.rs:
// File: crates/shop-api/src/middleware/mod.rs
pub mod cors;
pub mod enrich_error;
pub mod metrics_layer;
pub mod panic_catch; // NEW B84
pub mod rate_limit;
pub mod request_id;
pub mod security_headers;
pub mod timeout_layer;
pub mod trace_layer;
pub use panic_catch::panic_catch_layer; // NEW B84
Wire vào router INNERMOST nhất (gần handler nhất) để bao trùm mọi handler:
// File: crates/shop-api/src/router.rs (extract)
use crate::middleware::panic_catch::panic_catch_layer;
pub fn build_router(state: AppState) -> Router {
Router::new()
// ... routes + sub-routers + state
.fallback(handlers::fallback::not_found)
.method_not_allowed_fallback(handlers::fallback::method_not_allowed)
// INNER 11 — panic_catch (innermost, sát handler nhất)
.layer(panic_catch_layer())
// INNER 10 — timeout per-route (B82)
// INNER 9 — metrics_layer (B81)
// ... (8 layer còn lại B39-B80)
}
Lock pattern Shop API 3 điểm:
- Log panic
ERRORvớipanic_msgđầy đủ: server log là source of truth duy nhất để debug, không che giấu. Loki query{level="error"} |= "handler panicked"trả ra mọi panic event với timestamp +request_id(đã có từ span context B80). - Response KHÔNG expose
panic_msg: panic message thường có file path local (vd"index out of bounds: the len is 5 but the index is 100, called at src/handlers/products.rs:42") — tiết lộ cấu trúc source code, version, dependencies. OWASP A05 Security Misconfiguration cấm leak nội bộ ra client. Pattern: log đầy đủ server-side, mask client-side. - INNERMOST nhất để bắt mọi panic gần handler. Đặt outer hơn timeout (B82) hoặc metrics (B81) sẽ miss panic xảy ra trong handler — vì stack unwind đã đi lên qua các layer outer rồi.
Uniform Error Envelope Architecture
Sau B84, Shop API có 3 entry point sinh error response — đặt cạnh nhau để thấy architecture đầy đủ:
- Handler error path: handler trả
Result<T, AppError>. Rejection chuyển sang response quaimpl IntoResponse for AppError(B16 lock) — mapping 22 variant (sau B82) sang status code + body envelope + header phụ trợ (vdWWW-Authenticate,Retry-After). - Fallback path: request không match route →
not_found; sai method →method_not_allowed. Cả 2 trả envelope chuẩn (B84 lock). - Panic path: handler panic không expected →
CatchPanicLayercustom handler trả 500 (B84 lock).
Envelope chuẩn 3 path đều conform:
{
"error": "human-readable message",
"code": "MACHINE_CODE",
"request_id": "abc-123",
"detail": { ... }
}
4 field cố định mặc dù detail optional + varies theo error type:
- Handler error path:
detail.fieldscho ValidationFailed (B41),detail.timeout_secondscho Timeout (B82),detail.retry_aftercho RateLimited. - Fallback path:
detail.method+detail.uri+detail.hint. - Panic path:
detailKHÔNG có (mask panic_msg).
All 3 path enriched qua enrich_error_response middleware (B39 lock continued): middleware này wrap response, nếu response status >= 400 thì rewrite body chèn request_id từ X-Request-Id request header (hoặc x-request-id response header set bởi request_id middleware B39 nếu client không gửi). Pattern: handler không cần biết request_id để tự inject — middleware tự inject sau.
Verify lock pattern client side: parse body.error + body.code consistent cho mọi path bất kể error xuất phát từ đâu. Frontend code sample:
// Client-side TypeScript pseudo-code
if (!response.ok) {
const err = await response.json();
// err.error: hiển thị toast cho user
// err.code: filter theo loại (vd "VALIDATION_FAILED" show inline form error)
// err.request_id: copy vào support chat
// err.detail: zoom-in cho dev console
throw new ApiError(err);
}
Verify End-To-End + Edge Cases
4 scenario test verify uniform envelope cho 3 path:
Test 1 — Typo route → 404 JSON:
curl -i http://localhost:3000/api/v1/produkts
# < HTTP/1.1 404 Not Found
# < Content-Type: application/json
# < x-request-id: f1c8a3b7-...
# <
# {
# "error": "route not found",
# "code": "NOT_FOUND",
# "request_id": "f1c8a3b7-...",
# "detail": {
# "method": "GET",
# "uri": "/api/v1/produkts",
# "hint": "check spelling or refer to API docs at /api/docs"
# }
# }
Test 2 — Wrong method → 405:
curl -i -X DELETE http://localhost:3000/api/v1/products
# /api/v1/products có GET + POST, không có DELETE cho collection
# < HTTP/1.1 405 Method Not Allowed
# < Allow: GET, POST
# < Content-Type: application/json
# < x-request-id: 2d9e1f4a-...
# <
# {
# "error": "method not allowed for this route",
# "code": "METHOD_NOT_ALLOWED",
# "request_id": "2d9e1f4a-...",
# "detail": {
# "uri": "/api/v1/products",
# "hint": "check API docs for allowed methods on this endpoint"
# }
# }
Test 3 — OPTIONS preflight CORS:
curl -i -X OPTIONS http://localhost:3000/api/v1/products \
-H 'Origin: https://frontend.blogcode.vn' \
-H 'Access-Control-Request-Method: POST'
# < HTTP/1.1 204 No Content
# < Access-Control-Allow-Origin: https://frontend.blogcode.vn
# < Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
# < Access-Control-Allow-Headers: content-type, authorization, x-request-id, idempotency-key
# < Access-Control-Max-Age: 3600
Verify: CorsLayer B77 trả 204 trước khi đi qua router → handler method_not_allowed KHÔNG bị gọi → không có body. Đây là behavior mong muốn cho CORS preflight chuẩn fetch spec.
Test 4 — Panic catch. Thêm test handler giả lập panic (chỉ enable trong cfg(debug_assertions) để KHÔNG ship production):
// File: crates/shop-api/src/routes/test_panic.rs (dev-only)
#[cfg(debug_assertions)]
async fn panic_handler() -> &'static str {
panic!("oops something went wrong");
}
#[cfg(debug_assertions)]
pub fn routes() -> axum::Router<crate::state::AppState> {
use axum::routing::get;
axum::Router::new().route("/test/panic", get(panic_handler))
}
Test bằng curl + check log:
curl -i http://localhost:3000/test/panic
# < HTTP/1.1 500 Internal Server Error
# < Content-Type: application/json
# < x-request-id: 8a4f9c2e-...
# <
# {
# "error": "internal server error",
# "code": "INTERNAL_ERROR",
# "request_id": "8a4f9c2e-..."
# }
# Server log đồng thời:
# 2026-06-16T03:14:22Z ERROR shop_api::middleware::panic_catch:
# handler panicked panic="oops something went wrong" request_id="8a4f9c2e-..."
Verify pattern: client thấy generic 500 + envelope không expose panic message; server log đủ thông tin debug. Trace ID consistent giữa response + log → kỹ sư copy request_id từ user support chat, query Loki {request_id="8a4f9c2e-..."} ra ngay span đầy đủ. Đây là điểm cuối quan trọng nhất của bài: 4 scenario trên đều có shape {error, code, request_id} consistent.
Tổng Kết
- 3 entry point error: handler (
AppError), fallback (404/405), panic (500). - Uniform envelope
{error, code, request_id, detail?}cho mọi path. not_foundfallback + logWARN+ hint chung (avoid info disclosure).method_not_allowedfallback +Allowheader hint.- OPTIONS handler KHÔNG cần custom —
CorsLayerB77 đủ. CatchPanicLayerlast resort defense — log panicERROR, response masked.- Panic message KHÔNG expose trong response (security OWASP A05).
request_idenrich qua middleware B39 cho mọi error path.Allowheader từ axum tự generate (cho route đã define) — handler KHÔNG cần list method.- File path lock: extend
handlers/fallback.rs(refactor từ B19 placeholder); NEWmiddleware/panic_catch.rs. - Stack giờ 11 layer (10 cũ B82 +
panic_catchinnermost nhất). - AppError variant count: 22 KHÔNG đổi B82 — B84 không thêm variant, panic path dùng generic
INTERNAL_ERRORcode; not_found/method_not_allowed dùng JSON inline không quaAppErrorvì axum fallback signature trảResponsetrực tiếp. - Foundation cho B85 (test middleware stack), G18 production debug log filter.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 3 entry point error Shop API — pros/cons mỗi path. Tại sao uniform envelope MANDATORY?
not_foundlogWARNvsERROR— phân biệt severity. Cho ví dụ scenario alert filter.- OPTIONS handler tự động qua CORS — preflight flow? Khi nào cần custom OPTIONS?
CatchPanicLayerlast resort — tại sao MANDATORY trong production? Cho ví dụ scenario kill thread.- Panic message expose trong response — pitfall security. OWASP A02 information disclosure scenario.
Đáp án
- 3 entry point error pros/cons: (a) Handler error path (
AppErrorIntoResponseB16) — pros: handler trảResult<T, AppError>compile-time safe (compiler bắt mọi nhánh error chưa xử lý), 22 variant map đầy đủ status code + body + header phụ trợ (WWW-Authenticatecho 401,Retry-Aftercho 429/503,Allowcho 405,detail.timeout_secondscho 504), client biết rõ semantic; cons: chỉ catch error trong path handler, không catch route không tồn tại (chưa đến handler) + không catch panic. (b) Fallback path (404/405 B84) — pros: catch mọi request không match route trong toàn router tree, uniform envelope logWARNkhông bão alert filter; cons: chỉ catch routing failure không catch panic xảy ra trong handler;method_not_allowedkhông biếtallowed_methodsqua API hiện tại của axum 0.8 nên hint chung. (c) Panic path (CatchPanicLayerB84) — pros: last resort defense bao trùm mọi panic ngẫu nhiên (unwrap,arr[idx],.expect()) — không thread bị kill, response 500 envelope chuẩn, server log đầy đủ panic_msg để debug; cons: panic là code bug không nên rely on catch panic làm bộ đỡ — bug phải fix root cause (auditunwrap, dùng?+AppError); panic catch tạo overheadcatch_unwindmỗi request (nhỏ ~50ns nhưng hiện diện). Tại sao uniform envelope MANDATORY: (i) client code 1 đoạn parse cho mọi error không phải case-by-case theo status code; (ii) log aggregator Loki/Datadog/Honeycomb parse 1 schema duy nhất, query{code="NOT_FOUND"}filter chính xác; (iii) support chat dùngrequest_idconsistent search log; (iv) i18n table key theocodekhông phải free-texterror; (v) error tracking Sentry/Rollbar group event theocodekhông phải fingerprint từ message; (vi) front-end design system mappingcode→ toast icon/color/action button — không có envelope = front-end phảiif status === 404 elseif status === 405 elseif status === 500 ...rải rác. - 404 log
WARNvsERRORphân biệt severity: 404 không phải bug server — phổ biến do (a) user gõ sai URL, (b) link cũ cached Google search index 6 tháng trước, (c) bot scan path attack surface (/admin,/wp-login.php,/.env,/.git/config), (d) feature flag rollback ẩn route khỏi production nhưng frontend client cached cũ vẫn gọi, (e) typo trong tài liệu API redirect user sai. Đặt levelERRORsẽ flood alert filterlevel="error"noise nhấn chìm thật bug 5xx — kỹ sư on-call mất khả năng phân biệt critical vs noise → alert fatigue → ignore tất → 5xx thật miss SLA. Pattern lock Shop API: 4xx (client error) →WARN, 5xx (server error) →ERROR. Scenario alert filter cụ thể: Grafana alert rulerate({level="error"}[5m]) > 0.1trigger PagerDuty P2; nếu 404 đặtERRORthì 1 bot scan 1000 path trong 1 phút sẽ trigger alert sai dù service hoàn toàn healthy. Đúng patternWARN: bot scan tăng WARN rate nhưng KHÔNG trigger alert; rule riêngrate({level="warn", code="NOT_FOUND"}[5m]) > 10trigger Slack notification non-paging (chỉ thông báo, không gọi điện thoại on-call), kỹ sư xem khi rảnh decide block bot IP hay refactor route docs. Phân biệt 2 channel: alert paging cho P1/P2 + alert non-paging cho info — không gộp chung. - OPTIONS handler tự động qua CORS — preflight flow: browser fetch API gửi cross-origin request với method KHÔNG simple (POST với
Content-Type: application/json, PUT, PATCH, DELETE) hoặc header KHÔNG simple (Authorization,X-Request-Id,Idempotency-Key) → trước khi gửi request thật browser tự gửi preflight OPTIONS với headerOrigin: https://frontend.blogcode.vn+Access-Control-Request-Method: POST+Access-Control-Request-Headers: content-type, authorization; CorsLayer B77 nhận preflight (detect quaMethod::OPTIONS+ có headerOrigin+Access-Control-Request-Method), check allowlist origin + method + header, nếu pass trả204 No Content+ 4 header response (Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Max-Age: 3600); browser cache preflight 1h theoMax-Agekhông gửi lại cho request tiếp; sau đó browser gửi request thật (POST) với headerOrigin, server response thêmAccess-Control-Allow-Origin, browser cho phép JS đọc body. Khi nào cần custom OPTIONS: (a) endpoint OPTIONS RFC 9110 query khả năng route (rất hiếm — phần lớn API hiện đại không support); (b) endpoint same-origin không qua CorsLayer (vd/internal/adminchỉ cho IP allowlist) nhưng client muốn OPTIONS discover method — chỉ implement nếu thật sự có client cần; (c) custom CORS logic ngoài CorsLayer capability (vd allow origin theo regex pattern phức tạp, hoặc allow header dynamic theo user role) — implement middleware riêng wrap CorsLayer. Shop API: 99% case dùng CorsLayer default B77 đủ, KHÔNG custom OPTIONS. CatchPanicLayerMANDATORY production tại sao: Rust không có exception nhưngpanic!tồn tại — handler có thể panic do (a).unwrap()trênOption::None/Result::Err(vd config field bắt buộc nhưng env var thiếu, đáng lẽ phải fail tại startup nhưng dev viếtstd::env::var("X").unwrap()trong handler lazy), (b) array index out of bound (arr[100]khiarr.len() == 50), (c) divide by zero (integer overflow trong build release), (d).expect()với message hard-coded khi assumption broken (vdrow.get::<String, _>("name").expect("column name must exist")— sau migration drop column), (e) third-party crate panic do bug upstream chưa fix (vd image crate panic trên malformed JPEG). Không cóCatchPanicLayer: tokio runtime kill task tại điểm panic, axum trả empty 500 + connection close, panic message KHÔNG vào log nếu chưa setstd::panic::set_hook— kỹ sư mò mẫm reproduce không biết panic ở handler nào. Scenario kill thread cụ thể: handler list_products gọiproducts.iter().find(|p| p.id == target_id).unwrap()— assume target_id luôn tồn tại; production data có row deleted soft sau cache — handler panic, axum dispatch task panic, response empty 500 + connection close keep-alive break; user F5 lại — request tiếp phải bắt tay TLS lại (300ms thêm); 10k user/s panic = 10k connection drop/s, ALB AWS report unhealthy target, trigger auto-scaling spam scale instance để cover request không phục vụ được (thật sự là code bug 1 dòng). VớiCatchPanicLayer: panic catch tại layer, convert sang 500 envelope chuẩn + logERRORkèmrequest_id+ connection keep-alive được giữ; kỹ sư Loki query{level="error"} |= "handler panicked"ra ngay 100% panic event 5 phút qua, fix root cause (đổi.unwrap()sang?+ returnAppError::NotFound). Pattern lock: panic catch là tầng defense in depth KHÔNG thay thế fix bug — fix root cause vẫn ưu tiên, panic catch giữ service không kill thread + capture observability đầy đủ.- Panic message expose response pitfall security: panic message Rust thường chứa thông tin nội bộ source code không nên leak — OWASP A05 Security Misconfiguration + A02 Cryptographic Failures/info disclosure baseline. Scenario cụ thể: (a) Source code path panic message format
"index out of bounds: the len is 5 but the index is 100, called at /home/build/shop/crates/shop-api/src/handlers/products.rs:42"— attacker thấy đường dẫn build server (Docker container layer), source code crate name, file structure, version Rust toolchain (panic format thay đổi theo version) → map surface để target CVE specific. (b) Database structure: panic từ sqlx"no row found in scope"+"column 'admin_secret_token' does not exist"— leak schema database, attacker biết bảng có column nào để target SQL injection cụ thể. (c) Credential:.expect("DATABASE_URL must be set")panic message rò rỉ env var name + format URL connection (postgres://user:password@host:5432/dbnếu code viếtpanic!("invalid url: {}", url)) — leak credential plain text vào response. (d) Dependency version: panic từ jsonwebtoken"InvalidSignature: key length 256 doesn't match algorithm HS512"leak algorithm + key length thông tin tấn công crypto. (e) Business logic:.expect("user must be admin to access this")leak authorization rule cấu trúc app. Mitigate Shop API B84:handle_panicdowncast panic payload sang&str/Stringđể logERRORserver-side đầy đủ (debug được), response trả body envelope generic{"error":"internal server error","code":"INTERNAL_ERROR","request_id":"..."}KHÔNG nhúngpanic_msg. Pattern lock cho mọi 5xx Shop API: log đầy đủ server-side, response client mask không expose stack/path/dep version. Tương tự choAppError::Internal(String)B16 lock — message Internal đã sanitize lúc convert từ source error (vdsqlx::Error→AppError::Internal("database error".into())+ log raw error quatracing::error!), không bao giờformat!("{:?}", source_err)rồi nhúng vào response. Mỗi PR thêmAppError::Internalmới phải review xác nhận message KHÔNG leak internal — checklist 1 dòng PR template.
Bài Tiếp Theo
Bài 85: Middleware Integration Test — bài CUỐI Group 8: integration test toàn middleware stack (11 layer) qua testcontainers + tower::oneshot, assert order CORS preflight + security headers + rate limit + body limit + trace + metrics + timeout + validation + fallback + panic, tổng kết Group 8 (10/10).
