今天的教學內容包含:
為什麼要學這些?
其中,Path以及Query在前一天已經使用過,範例可以參考當時的程式碼。
作用是從路徑取得參數
注意:
作用是取得查詢字串
注意:
作用是讀取與檢查標頭,接下來的範例將示範如何讀取 HTTP header(Authorization)並回傳處理結果。
範例程式碼
use axum::{
http::HeaderMap,
response::{IntoResponse, Json, Response},
routing::get,
Router,
};
use serde::Serialize;
use tokio::net::TcpListener;
#[derive(Serialize)]
struct Profile {
username: String,
}
async fn profile(headers: HeaderMap) -> Response {
if let Some(value) = headers.get("authorization") {
if let Ok(s) = value.to_str() {
if s.starts_with("Bearer ") {
let profile = Profile {
username: "user1".to_string(),
};
return (axum::http::StatusCode::OK, Json(profile)).into_response();
}
}
}
(axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response()
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/profile", get(profile));
let listener = TcpListener::bind("127.0.0.1:3000")
.await
.expect("Failed to bind to address");
println!("Server running on http://127.0.0.1:3000");
axum::serve(listener, app)
.await
.expect("Failed to start server");
}
輸入cargo run開始測試:
首先我們將輸入:
curl -X GET "http://127.0.0.1:3000/profile" -H "Authorization: Bearer TOKEN123" -H "Content-Type: application/json" -v
我們可以得到以下的輸出,還可以看到200的狀態碼:
* Trying 127.0.0.1:3000...
* Connected to 127.0.0.1 (127.0.0.1) port 3000
> GET /profile HTTP/1.1
> Host: 127.0.0.1:3000
> User-Agent: curl/8.4.0
> Accept: */*
> Authorization: Bearer TOKEN123
> Content-Type: application/json
>
< HTTP/1.1 200 OK
< content-type: application/json
< content-length: 20
< date: Wed, 24 Sep 2025 03:07:08 GMT
<
{"username":"user1"}* Connection #0 to host 127.0.0.1 left intact
換成沒有帶上Bearer,測試一下:
curl -X GET "http://127.0.0.1:3000/profile" -H "Authorization: Nothing" -H "Content-Type: application/json"
這次的輸出就不同了,狀態碼也換成401
* Trying 127.0.0.1:3000...
* Connected to 127.0.0.1 (127.0.0.1) port 3000
> GET /profile HTTP/1.1
> Host: 127.0.0.1:3000
> User-Agent: curl/8.4.0
> Accept: */*
> Authorization: Nothing
> Content-Type: application/json
>
< HTTP/1.1 401 Unauthorized
< content-type: text/plain; charset=utf-8
< content-length: 12
< date: Wed, 24 Sep 2025 03:06:00 GMT
<
Unauthorized* Connection #0 to host 127.0.0.1 left intact
當然真正的專案中,授權與驗證沒有這麽隨便,這邊簡化了驗證部分。
接下來,我們將完成一個具有自訂提取器的Axum專案。
範例程式碼
use axum::{
extract::FromRequestParts,
http::{request::Parts, StatusCode},
response::Json,
routing::get,
Router,
};
use serde::Serialize;
use tokio::net::TcpListener;
#[derive(Debug)]
struct AuthUser {
user_id: i64,
}
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
if let Some(auth) = parts.headers.get("authorization") {
if let Ok(s) = auth.to_str() {
if let Some(token) = s.strip_prefix("Bearer ") {
if token.starts_with("user:") {
if let Ok(id) = token["user:".len()..].parse::<i64>() {
return Ok(AuthUser { user_id: id });
}
}
}
}
}
Err((StatusCode::UNAUTHORIZED, "Unauthorized"))
}
}
#[derive(Serialize)]
struct UserInfo {
user_id: i64,
message: String,
}
async fn get_user_info(auth_user: AuthUser) -> Json<UserInfo> {
Json(UserInfo {
user_id: auth_user.user_id,
message: format!("Hello, user {}!", auth_user.user_id),
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/user", get(get_user_info));
let listener = TcpListener::bind("127.0.0.1:3000")
.await
.expect("Failed to bind to address");
println!("Server running on http://127.0.0.1:3000");
println!("GET /user - Get user info (requires Bearer user:ID token)");
axum::serve(listener, app)
.await
.expect("Failed to start server");
}
讓我解釋各部分的功能:
#[derive(Debug)]
struct AuthUser {
user_id: i64,
}
impl<S> FromRequestParts<S> for AuthUser
// ...
// ...
// ...
這部分實作了自動從 HTTP 請求中提取認證資訊的邏輯:
認證流程**:**
程式定義了需要認證的端點:
這個端點會自動要求認證,因為它們的 handler 函數參數包含 AuthUser,如果沒有 AuthUser 將回傳401錯誤。
成功的範例:
curl -X GET "http://127.0.0.1:3000/user" -H "Authorization: Bearer user:123" -H "Content-Type: application/json" -v
輸出結果:
* Trying 127.0.0.1:3000...
* Connected to 127.0.0.1 (127.0.0.1) port 3000
> GET /user HTTP/1.1
> Host: 127.0.0.1:3000
> User-Agent: curl/8.4.0
> Accept: */*
> Authorization: Bearer user:123
> Content-Type: application/json
>
< HTTP/1.1 200 OK
< content-type: application/json
< content-length: 44
< date: Wed, 24 Sep 2025 03:52:08 GMT
<
{"user_id":123,"message":"Hello, user 123!"}* Connection #0 to host 127.0.0.1 left intact
失敗的範例,會收到401,因為不符合 user:數字 的格式:
curl -X GET "http://127.0.0.1:3000/user" -H "Authorization: Bearer user:nothing" -H "Content-Type: application/json" -v
我們接下來要完成一個處理用戶註冊表單的伺服器,能夠接收表單資料並返回 JSON 響應。
表單資料結構體 (SignupForm)
響應結構體 (SignupResp)
表單處理函數(signup函數)
SignupForm
結構用戶送出表單
↓
POST /submit
Content-Type: application/x-www-form-urlencoded
↓
Axum 接收請求
↓
Form 提取器解析資料
↓
資料符合 SignupForm 格式嗎?
├─ 否 → 返回 422 Unprocessable Entity
└─ 是 → 繼續
↓
執行 signup 函數
↓
建立 SignupResp
↓
序列化為 JSON
↓
返回給客戶端
use axum::{
extract::Form,
response::Json,
routing::post,
Router,
};
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;
#[derive(Deserialize)]
struct SignupForm {
name: String,
age: Option<u8>,
}
#[derive(Serialize)]
struct SignupResp {
success: bool,
name: String,
}
async fn signup(Form(payload): Form<SignupForm>) -> Json<SignupResp> {
Json(SignupResp {
success: true,
name: payload.name,
})
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/submit", post(signup));
let listener = TcpListener::bind("127.0.0.1:3000")
.await
.expect("Failed to bind to address");
println!("Server running on http://127.0.0.1:3000");
println!("POST /submit - Submit signup form");
axum::serve(listener, app)
.await
.expect("Failed to start server");
}
完整表單(包含所有欄位)
curl -X POST http://127.0.0.1:3000/submit -H "Content-Type: application/x-www-form-urlencoded" -d "name=user1&age=114"
# 回傳內容
{"success":true,"name":"user1"}
如果去除age部分,回傳的內容不會改變,因為age是可省略的欄位
curl -X POST http://127.0.0.1:3000/submit -H "Content-Type: application/x-www-form-urlencoded" -d "age=114"
# 回傳內容
Failed to deserialize form body: missing field `name`
現在,我們來完成一個有自動資料驗證的 RESTful API,在處理用戶註冊前會先驗證所有輸入資料的格式和規則。
驗證規則定義 (CreateUserRequest)
各欄位的驗證規則:
Derive 巨集:
錯誤響應結構 (ErrorResp)
驗證處理邏輯(create_user函數)
客戶端發送 JSON 請求
↓
POST /users
Content-Type: application/json
↓
Axum 解析 JSON → 失敗 → 400 Bad Request
↓ 成功
建立 CreateUserRequest 物件
↓
執行 validate() 方法
↓
檢查所有驗證規則
├─ name 長度 (2-30)
├─ email 格式
└─ password 長度 (≥8)
↓
所有規則都通過?
├─ 否 → 收集錯誤細節
│ ↓
│ 建立 ErrorResp
│ ↓
│ 返回 400 + 錯誤詳情
│
└─ 是 → 返回 200 + {"success": true}
use axum::{extract::Json, response::IntoResponse, routing::post, Router};
use serde::{Deserialize, Serialize};
use validator::Validate;
use tokio::net::TcpListener;
#[derive(Debug, Deserialize, Validate)]
struct CreateUserRequest {
#[validate(length(min = 2, max = 30))]
name: String,
#[validate(email)]
email: String,
#[validate(length(min = 8))]
password: String,
}
async fn create_user(Json(payload): Json<CreateUserRequest>) -> impl IntoResponse {
if let Err(e) = payload.validate() {
let error_response = serde_json::json!({
"message": "Validation error",
"details": e.to_string()
});
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(error_response));
}
(axum::http::StatusCode::OK, axum::Json(serde_json::json!({"success": true})))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/users", post(create_user));
let listener = TcpListener::bind("127.0.0.1:3000")
.await
.expect("Failed to bind to address");
println!("Server running on http://127.0.0.1:3000");
println!("POST /users - Create user with validation");
axum::serve(listener, app)
.await
.expect("Failed to start server");
}
我們需要在cargo.toml中,添加依賴套件:
serde_json = "1"
validator = { version = "0.20", features = ["derive"] }
無錯誤的範例
curl -X POST http://127.0.0.1:3000/users -H "Content-Type: application/json" -d "{\"name\": \"user1\", \"email\": \"user1@aaa.com\", \"password\": \"12345678\"}"
# 結果
{"success":true}
錯誤:名稱過短
curl -X POST http://127.0.0.1:3000/users -H "Content-Type: application/json" -d "{\"name\": \"a\", \"email\": \"user1@aaa.com\", \"password\": \"12345678\"}"
# 結果
{"details":"name: Validation error: length [{\"max\": Number(30), \"min\": Number(2), \"value\": String(\"a\")}]","message":"Validation error"}
多個欄位出錯(名稱過短、email格式錯誤、密碼是空的)
curl -X POST http://127.0.0.1:3000/users -H "Content-Type: application/json" -d "{\"name\": \"a\", \"email\": \"not-email\", \"password\": \"\"}"
#結果
{"details":"name: Validation error: length [{\"min\": Number(2), \"max\": Number(30), \"value\": String(\"a\")}]\nemail: Validation error: email [{\"value\": String(\"not-email\")}]\npassword: Validation error: length [{\"min\": Number(8), \"value\": String(\"\")}]","message":"Validation error"}
除了我們使用到的規則,還支援的以下的格式:
// 數字範圍
#[validate(range(min = 18, max = 100))]
age: u8,
// 正規表示式
#[validate(regex = "^[A-Z]{2}[0-9]{4}$")]
text: String,
// URL 格式
#[validate(url)]
website: String,
// 必須包含特定內容
#[validate(contains = "@")]
email_alt: String,