在建構任何生產級別的 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_id和course_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。