在建構任何生產級別的 Web API 時,輸入驗證與錯誤處理都是確保系統穩健性與安全性的基本。一個優秀的 API 應該能優雅地拒絕無效輸入,並在發生錯誤時,回傳清晰、一致且不洩漏內部實作細節的錯誤訊息。本文將深入探討如何運用 validator 套件在 Actix-web 中實現宣告式的輸入驗證,並設計一套自定義錯誤處理機制,最終整合日誌記錄,打造出專業且可靠的 API。

步驟一:使用 validator 進行宣告式驗證

傳統上,在 handler 函式中編寫大量的 if-else 檢查是枯燥且容易出錯的。validator 套件讓我們能以宣告式的方式,直接在資料結構上定義驗證規則。

1. 加入依賴

首先,在 Cargo.toml 中加入 validator

[dependencies]
validator = { version = "0.16", features = ["derive"] }
# ... 其他依賴

2. 在結構體上定義驗證規則

假設我們要驗證一個 API 路徑參數,我們可以這樣定義結構體:

use serde::Deserialize;
use validator::Validate;

#[derive(Deserialize, Validate)]
pub struct CourseTutorPath {
    #[validate(range(min = 1, message = "Tutor ID 必須為正整數"))]
    pub tutor_id: u32,
    #[validate(range(min = 1, message = "Course ID 必須為正整數"))]
    pub course_id: u32,
}

程式碼解說

  • #[derive(Validate)] 啟用 validator 的功能。
  • #[validate(...)] 屬性宏讓我們能為每個欄位加上驗證規則。此處我們要求 tutor_idcourse_id 都必須是大于等於 1 的正整數。

3. 在 Handler 中執行驗證

在 Actix-web 的 handler 中,我們只需呼叫 .validate() 方法即可觸發驗證。

use actix_web::{web, HttpResponse, Responder};

pub async fn get_course_detail(
    path: web::Path<CourseTutorPath>,
) -> impl Responder {
    match path.validate() {
        Ok(_) => {
            // 驗證通過,執行業務邏輯
            HttpResponse::Ok().json(format!(
                "成功取得 Tutor ID: {} 的 Course ID: {} 詳細資料",
                path.tutor_id, path.course_id
            ))
        }
        Err(e) => {
            // 驗證失敗,回傳錯誤訊息
            HttpResponse::BadRequest().json(e.to_string())
        }
    }
}

雖然這樣可行,但將錯誤處理邏輯散落在每個 handler 中並不理想。我們的目標是建立一個更優雅的全局錯誤處理機制。

步驟二:設計自定義錯誤型別

為了統一處理來自不同來源(如驗證、資料庫、業務邏輯)的錯誤,並回傳一致的 HTTP 回應,我們需要設計一個自定義的錯誤型別。

1. 定義 UserError 列舉

src/errors.rs 中,我們定義一個 UserError 列舉來涵蓋應用中可能出現的各種錯誤情況。

use actix_web::ResponseError;
use derive_more::Display;

#[derive(Debug, Display)]
pub enum UserError {
    #[display(fmt = "輸入驗證失敗: {}", _0)]
    ValidationError(String),

    #[display(fmt = "找不到指定的資源")]
    NotFoundError,

    #[display(fmt = "內部伺服器錯誤,請稍後再試")]
    UnexpectedError,
}

derive_more::Display 讓我們能方便地為每個錯誤變體定義其字串表示。

2. 為 UserError 實現 ResponseError

這是最關鍵的一步。透過實現 ResponseError trait,我們告訴 Actix-web 如何將我們的 UserError 轉換為一個標準的 HttpResponse

use actix_web::{http::StatusCode, HttpResponse};
use serde_json::json;

impl ResponseError for UserError {
    fn status_code(&self) -> StatusCode {
        match self {
            UserError::ValidationError(_) => StatusCode::BAD_REQUEST,
            UserError::NotFoundError => StatusCode::NOT_FOUND,
            UserError::UnexpectedError => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code())
            .json(json!({ "error": self.to_string() }))
    }
}

程式碼解說

  • status_code(): 根據不同的錯誤變體,回傳對應的 HTTP 狀態碼。
  • error_response(): 定義錯誤回應的 body 格式。這裡我們統一使用 JSON 格式,並包含一個 error 欄位。

步驟三:重構 Handler 並整合錯誤處理

現在,我們可以重構 handler,使其回傳 Result<_, UserError>,讓 Actix-web 自動處理錯誤轉換。

// 在 handler 檔案中
use super::errors::UserError; // 引入自定義錯誤

pub async fn get_course_detail(
    path: web::Path<CourseTutorPath>,
) -> Result<impl Responder, UserError> {
    // 執行驗證,並將錯誤轉換為我們的自定義型別
    path.validate().map_err(|e| UserError::ValidationError(e.to_string()))?;

    // ... 假設的業務邏輯 ...
    // let course = find_course(path.tutor_id, path.course_id)?;
    // if course.is_none() {
    //     return Err(UserError::NotFoundError);
    // }

    Ok(HttpResponse::Ok().json(format!(
        "成功取得 Tutor ID: {} 的 Course ID: {} 詳細資料",
        path.tutor_id, path.course_id
    )))
}

程式碼解說

  • Handler 的回傳型別變為 Result<impl Responder, UserError>
  • 我們使用 ? 運算子。當 path.validate() 回傳 Err 時,map_err 會將其轉換為 UserError::ValidationError,然後 ? 會立即將這個錯誤從函式中回傳。
  • Actix-web 捕捉到回傳的 Err(UserError) 後,會自動呼叫我們為 UserError 實現的 error_response() 方法來生成最終的 HTTP 回應。

圖表解說:自定義錯誤處理流程

此循序圖展示了當輸入驗證失敗時,錯誤如何被轉換並由 Actix-web 處理為標準 HTTP 回應的完整流程。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title 驗證失敗時的錯誤處理流程

participant "Actix-web" as Actix
participant "Handler" as Handler
participant "Validator" as Validator
participant "UserError" as UserError

Actix -> Handler : 呼叫 handler
Handler -> Validator : path.validate()
Validator --> Handler : 回傳 Err(ValidationErrors)
Handler -> UserError : 將 ValidationErrors\n轉換為 UserError::ValidationError
Handler --> Actix : 回傳 Result::Err(UserError)
Actix -> UserError : 呼叫 .error_response()
UserError --> Actix : 產生 HttpResponse (400 Bad Request)
Actix --> client : 回傳 HTTP 400 回應

@enduml

步驟四:整合日誌記錄

最後,為了方便除錯與監控,我們應加入日誌記錄。

main.rs 中初始化 env_logger 並啟用 Logger 中介軟體:

use actix_web::middleware::Logger;
use std::env;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env::set_var("RUST_LOG", "info"); // 設定預設日誌級別
    env_logger::init();

    HttpServer::new(|| {
        App::new()
            .wrap(Logger::default()) // 加入 Logger 中介軟體
            // ... 註冊路由 ...
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

現在,每一次請求和由 ResponseError 產生的錯誤回應,都會被自動記錄下來,極大地提升了應用的可維護性。透過這套組合拳,我們便能建構出既安全又穩健的 Actix-web API。