Danh sách bài viết

Bài 84: Fallback Handler — 404, 405, OPTIONS Uniform Response

Bài 84 của series Rust RESTful API — bài thứ 9 Group 8 Middleware Sâu (B84/B85), implement đầy đủ fallback handler cho Shop API trên top của B19 preview placeholder qua 3 entry point error uniform envelope: handler error path (AppError IntoResponse B16 lock) + fallback path (not_found 404 + method_not_allowed 405 B84 mới) + panic path (CatchPanicLayer last resort defense B84 mới); refactor crates/shop-api/src/handlers/fallback.rs từ B19 placeholder thành đầy đủ envelope chuẩn (log WARN không ERROR vì user typo không phải bug + detail.method + detail.uri + detail.hint chung tránh suggestion attack OWASP A05 information disclosure); method_not_allowed handler manual hint chung vì axum chưa expose dễ dàng allowed methods qua API; OPTIONS handler auto qua CorsLayer B77 lock KHÔNG cần custom (preflight Access-Control-Allow-Methods/Access-Control-Allow-Headers đầy đủ); NEW crates/shop-api/src/middleware/panic_catch.rs với tower_http::catch_panic::CatchPanicLayer custom handler downcast Box<dyn Any + Send + 'static> sang &str/String + log ERROR với panic message server-side + response masked KHÔNG expose panic message OWASP A05 prevention; wire panic_catch_layer INNERMOST nhất để catch panic gần handler; uniform envelope {error, code, request_id, detail?} cho mọi error path; request_id enrich qua middleware B39 lock continued cho mọi 3 path; stack giờ 11 layer (10 cũ B82 + panic_catch innermost); foundation cho B85 integration test middleware stack + G18 production debug log filter.

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

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

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

  • Implement not_found fallback thay axum default text "Not Found".
  • Implement method_not_allowed fallback với Allow header 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), NEW middleware/panic_catch.rs.
  • Stack giờ 11 layer (10 cũ B82 + panic_catch innermost).
2

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 Found dạng text/plain, không có header phụ trợ, không có envelope.
  • 405 Method Not Allowed: empty body với header Allow: GET, POST liệ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ọi response.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_id KHÔ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ều request_id duy 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.

3

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 WARN KHÔNG ERROR: 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 level ERROR sẽ flood alert filter level="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 %20 giữa slug).
  • detail.hint chung — 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
}
4

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_methods của route đang fallback. Nếu tự duy trì 1 bảng HashMap<path, Vec<Method>> song song với router definition → 2 nguồn truth dễ lệch (thêm PATCH mới quên update bảng → client thấy 405 nói Allow: GET, POST nhưng thực tế server đã accept PATCH).
  • Pattern lock: tin Allow header axum tự generate (đúng 100% theo router definition) + body envelope chỉ hint chung "check API docs". Client tử tế đọc Allow header trước khi retry; client lười cần hint dẫ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.

5

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/DELETE cross-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, OPTIONS từ 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))
}
6

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 ERROR với panic_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.
7

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 qua impl IntoResponse for AppError (B16 lock) — mapping 22 variant (sau B82) sang status code + body envelope + header phụ trợ (vd WWW-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 → CatchPanicLayer custom 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.fields cho ValidationFailed (B41), detail.timeout_seconds cho Timeout (B82), detail.retry_after cho RateLimited.
  • Fallback path: detail.method + detail.uri + detail.hint.
  • Panic path: detail KHÔ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);
}
8

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.

9

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_found fallback + log WARN + hint chung (avoid info disclosure).
  • method_not_allowed fallback + Allow header hint.
  • OPTIONS handler KHÔNG cần custom — CorsLayer B77 đủ.
  • CatchPanicLayer last resort defense — log panic ERROR, response masked.
  • Panic message KHÔNG expose trong response (security OWASP A05).
  • request_id enrich qua middleware B39 cho mọi error path.
  • Allow header 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); NEW middleware/panic_catch.rs.
  • Stack giờ 11 layer (10 cũ B82 + panic_catch innermost nhất).
  • AppError variant count: 22 KHÔNG đổi B82 — B84 không thêm variant, panic path dùng generic INTERNAL_ERROR code; not_found/method_not_allowed dùng JSON inline không qua AppError vì axum fallback signature trả Response trực tiếp.
  • Foundation cho B85 (test middleware stack), G18 production debug log filter.
10

Bài Tập Củng Cố

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

  1. 3 entry point error Shop API — pros/cons mỗi path. Tại sao uniform envelope MANDATORY?
  2. not_found log WARN vs ERROR — phân biệt severity. Cho ví dụ scenario alert filter.
  3. OPTIONS handler tự động qua CORS — preflight flow? Khi nào cần custom OPTIONS?
  4. CatchPanicLayer last resort — tại sao MANDATORY trong production? Cho ví dụ scenario kill thread.
  5. Panic message expose trong response — pitfall security. OWASP A02 information disclosure scenario.
Đáp án
  1. 3 entry point error pros/cons: (a) Handler error path (AppError IntoResponse B16) — 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-Authenticate cho 401, Retry-After cho 429/503, Allow cho 405, detail.timeout_seconds cho 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 log WARN không bão alert filter; cons: chỉ catch routing failure không catch panic xảy ra trong handler; method_not_allowed không biết allowed_methods qua API hiện tại của axum 0.8 nên hint chung. (c) Panic path (CatchPanicLayer B84) — 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 (audit unwrap, dùng ? + AppError); panic catch tạo overhead catch_unwind mỗ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ùng request_id consistent search log; (iv) i18n table key theo code không phải free-text error; (v) error tracking Sentry/Rollbar group event theo code không phải fingerprint từ message; (vi) front-end design system mapping code → toast icon/color/action button — không có envelope = front-end phải if status === 404 elseif status === 405 elseif status === 500 ... rải rác.
  2. 404 log WARN vs ERROR phâ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 level ERROR sẽ flood alert filter level="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 rule rate({level="error"}[5m]) > 0.1 trigger PagerDuty P2; nếu 404 đặt ERROR thì 1 bot scan 1000 path trong 1 phút sẽ trigger alert sai dù service hoàn toàn healthy. Đúng pattern WARN: bot scan tăng WARN rate nhưng KHÔNG trigger alert; rule riêng rate({level="warn", code="NOT_FOUND"}[5m]) > 10 trigger 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.
  3. 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 header Origin: https://frontend.blogcode.vn + Access-Control-Request-Method: POST + Access-Control-Request-Headers: content-type, authorization; CorsLayer B77 nhận preflight (detect qua Method::OPTIONS + có header Origin + 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 theo Max-Age không gửi lại cho request tiếp; sau đó browser gửi request thật (POST) với header Origin, server response thêm Access-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/admin chỉ 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.
  4. CatchPanicLayer MANDATORY production tại sao: Rust không có exception nhưng panic! tồn tại — handler có thể panic do (a) .unwrap() trên Option::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ết std::env::var("X").unwrap() trong handler lazy), (b) array index out of bound (arr[100] khi arr.len() == 50), (c) divide by zero (integer overflow trong build release), (d) .expect() với message hard-coded khi assumption broken (vd row.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 set std::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ọi products.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ới CatchPanicLayer: panic catch tại layer, convert sang 500 envelope chuẩn + log ERROR kèm request_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 ? + return AppError::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 đủ.
  5. 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/db nếu code viết panic!("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_panic downcast panic payload sang &str/String để log ERROR server-side đầy đủ (debug được), response trả body envelope generic {"error":"internal server error","code":"INTERNAL_ERROR","request_id":"..."} KHÔNG nhúng panic_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ự cho AppError::Internal(String) B16 lock — message Internal đã sanitize lúc convert từ source error (vd sqlx::ErrorAppError::Internal("database error".into()) + log raw error qua tracing::error!), không bao giờ format!("{:?}", source_err) rồi nhúng vào response. Mỗi PR thêm AppError::Internal mới phải review xác nhận message KHÔNG leak internal — checklist 1 dòng PR template.
11

Bài Tiếp Theo

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