表单数据 #

表单类型 #

Actix Web 支持多种表单数据格式:

类型 Content-Type 说明
URL 编码 application/x-www-form-urlencoded 简单表单
多部分 multipart/form-data 文件上传
JSON application/json API 请求

URL 编码表单 #

基本用法 #

rust
use actix_web::{web, HttpResponse, Responder};
use serde::Deserialize;

#[derive(Deserialize)]
struct LoginForm {
    username: String,
    password: String,
    remember: Option<bool>,
}

#[actix_web::post("/login")]
async fn login(form: web::Form<LoginForm>) -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({
        "username": form.username,
        "remember": form.remember.unwrap_or(false),
        "success": true
    }))
}

复杂表单结构 #

rust
#[derive(Deserialize)]
struct Address {
    street: String,
    city: String,
    zip_code: String,
}

#[derive(Deserialize)]
struct RegistrationForm {
    name: String,
    email: String,
    address: Address,
    interests: Option<Vec<String>>,
}

#[actix_web::post("/register")]
async fn register(form: web::Form<RegistrationForm>) -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({
        "name": form.name,
        "email": form.email,
        "address": {
            "street": form.address.street,
            "city": form.address.city,
            "zip_code": form.address.zip_code
        }
    }))
}

表单验证 #

rust
use validator::Validate;

#[derive(Deserialize, Validate)]
struct ContactForm {
    #[validate(length(min = 2, max = 100))]
    name: String,
    
    #[validate(email)]
    email: String,
    
    #[validate(length(min = 10))]
    message: String,
}

#[actix_web::post("/contact")]
async fn contact(form: web::Form<ContactForm>) -> impl Responder {
    if let Err(e) = form.validate() {
        return HttpResponse::BadRequest().json(serde_json::json!({
            "error": e.to_string()
        }));
    }
    
    HttpResponse::Ok().json(serde_json::json!({
        "success": true
    }))
}

多部分表单 #

添加依赖 #

toml
[dependencies]
actix-multipart = "0.6"
tokio = { version = "1", features = ["fs"] }

基本多部分处理 #

rust
use actix_multipart::Multipart;
use futures::{StreamExt, TryStreamExt};

#[actix_web::post("/upload")]
async fn upload(mut payload: Multipart) -> impl Responder {
    while let Ok(Some(mut field)) = payload.try_next().await {
        let content_type = field.content_type().map(|m| m.to_string());
        let disposition = field.content_disposition();
        let filename = disposition
            .get_filename()
            .map(|s| s.to_string());
        
        let mut data = Vec::new();
        while let Ok(Some(chunk)) = field.try_next().await {
            data.extend_from_slice(&chunk);
        }
        
        println!("Received file: {:?}", filename);
        println!("Content type: {:?}", content_type);
        println!("Size: {} bytes", data.len());
    }
    
    HttpResponse::Ok().json(serde_json::json!({
        "status": "uploaded"
    }))
}

保存上传文件 #

rust
use actix_multipart::Multipart;
use futures::{StreamExt, TryStreamExt};
use std::io::Write;
use uuid::Uuid;

#[actix_web::post("/upload/file")]
async fn upload_file(mut payload: Multipart) -> impl Responder {
    let upload_dir = std::path::Path::new("uploads");
    std::fs::create_dir_all(upload_dir).ok();
    
    let mut uploaded_files = Vec::new();
    
    while let Ok(Some(mut field)) = payload.try_next().await {
        let filename = field
            .content_disposition()
            .get_filename()
            .map(|s| s.to_string())
            .unwrap_or_else(|| Uuid::new_v4().to_string());
        
        let filepath = upload_dir.join(&filename);
        let mut file = std::fs::File::create(&filepath).unwrap();
        
        while let Ok(Some(chunk)) = field.try_next().await {
            file.write_all(&chunk).unwrap();
        }
        
        uploaded_files.push(filename);
    }
    
    HttpResponse::Ok().json(serde_json::json!({
        "files": uploaded_files
    }))
}

异步文件保存 #

rust
use actix_multipart::Multipart;
use futures::{StreamExt, TryStreamExt};
use tokio::io::AsyncWriteExt;

#[actix_web::post("/upload/async")]
async fn upload_async(mut payload: Multipart) -> impl Responder {
    let upload_dir = std::path::Path::new("uploads");
    tokio::fs::create_dir_all(upload_dir).await.ok();
    
    let mut uploaded_files = Vec::new();
    
    while let Ok(Some(mut field)) = payload.try_next().await {
        let filename = field
            .content_disposition()
            .get_filename()
            .map(|s| s.to_string())
            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
        
        let filepath = upload_dir.join(&filename);
        let mut file = tokio::fs::File::create(&filepath).await.unwrap();
        
        while let Ok(Some(chunk)) = field.try_next().await {
            file.write_all(&chunk).await.unwrap();
        }
        
        uploaded_files.push(filename);
    }
    
    HttpResponse::Ok().json(serde_json::json!({
        "files": uploaded_files
    }))
}

混合表单数据 #

文件和文本字段 #

rust
use actix_multipart::Multipart;
use futures::{StreamExt, TryStreamExt};
use std::collections::HashMap;

#[actix_web::post("/submit")]
async fn submit_form(mut payload: Multipart) -> impl Responder {
    let mut fields: HashMap<String, String> = HashMap::new();
    let mut files: Vec<String> = Vec::new();
    
    while let Ok(Some(mut field)) = payload.try_next().await {
        let name = field
            .content_disposition()
            .get_name()
            .unwrap_or("")
            .to_string();
        
        if field.content_type().is_some() {
            let filename = field
                .content_disposition()
                .get_filename()
                .map(|s| s.to_string())
                .unwrap_or_default();
            
            let mut data = Vec::new();
            while let Ok(Some(chunk)) = field.try_next().await {
                data.extend_from_slice(&chunk);
            }
            
            files.push(filename);
        } else {
            let mut value = String::new();
            while let Ok(Some(chunk)) = field.try_next().await {
                value.push_str(&String::from_utf8_lossy(&chunk));
            }
            fields.insert(name, value);
        }
    }
    
    HttpResponse::Ok().json(serde_json::json!({
        "fields": fields,
        "files": files
    }))
}

表单配置 #

限制表单大小 #

rust
use actix_web::web::FormConfig;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .app_data(FormConfig::default()
                .limit(1024 * 1024)  // 1MB
                .error_handler(|err, _| {
                    actix_web::error::ErrorBadRequest(serde_json::json!({
                        "error": "Form too large"
                    }))
                })
            )
            .route("/submit", web::post().to(submit_form))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

多部分配置 #

rust
use actix_multipart::form::{MultipartForm, tempfile::TempFile, text::Text};

#[derive(MultipartForm)]
struct UploadForm {
    #[multipart(limit = "10MB")]
    file: TempFile,
    
    #[multipart(limit = "1KB")]
    description: Text<String>,
}

#[actix_web::post("/upload/structured")]
async fn upload_structured(form: MultipartForm<UploadForm>) -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({
        "description": form.description,
        "file_size": form.file.size
    }))
}

错误处理 #

表单解析错误 #

rust
use actix_web::error::UrlencodedError;

async fn form_error_handler(err: UrlencodedError, _: &HttpRequest) -> actix_web::Error {
    match err {
        UrlencodedError::Overflow { size, limit } => {
            actix_web::error::ErrorBadRequest(serde_json::json!({
                "error": "Form too large",
                "size": size,
                "limit": limit
            }))
        }
        UrlencodedError::ContentType => {
            actix_web::error::ErrorBadRequest(serde_json::json!({
                "error": "Invalid content type"
            }))
        }
        _ => actix_web::error::ErrorBadRequest(serde_json::json!({
            "error": err.to_string()
        }))
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .app_data(
                web::FormConfig::default()
                    .error_handler(form_error_handler)
            )
            .route("/submit", web::post().to(submit_form))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

完整示例 #

rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use actix_multipart::Multipart;
use futures::{StreamExt, TryStreamExt};
use serde::Deserialize;
use validator::Validate;

#[derive(Deserialize, Validate)]
struct ContactForm {
    #[validate(length(min = 2, max = 100))]
    name: String,
    
    #[validate(email)]
    email: String,
    
    #[validate(length(min = 10))]
    message: String,
}

#[derive(Deserialize)]
struct LoginForm {
    username: String,
    password: String,
}

#[actix_web::post("/login")]
async fn login(form: web::Form<LoginForm>) -> impl Responder {
    if form.username == "admin" && form.password == "secret" {
        HttpResponse::Ok().json(serde_json::json!({
            "success": true,
            "token": "abc123"
        }))
    } else {
        HttpResponse::Unauthorized().json(serde_json::json!({
            "error": "Invalid credentials"
        }))
    }
}

#[actix_web::post("/contact")]
async fn contact(form: web::Form<ContactForm>) -> impl Responder {
    if let Err(e) = form.validate() {
        return HttpResponse::BadRequest().json(serde_json::json!({
            "error": e.to_string()
        }));
    }
    
    HttpResponse::Ok().json(serde_json::json!({
        "success": true,
        "message": "Thank you for your message"
    }))
}

#[actix_web::post("/upload")]
async fn upload(mut payload: Multipart) -> impl Responder {
    let mut files = Vec::new();
    
    while let Ok(Some(mut field)) = payload.try_next().await {
        let filename = field
            .content_disposition()
            .get_filename()
            .map(|s| s.to_string())
            .unwrap_or_default();
        
        let mut size = 0;
        while let Ok(Some(chunk)) = field.try_next().await {
            size += chunk.len();
        }
        
        files.push(serde_json::json!({
            "name": filename,
            "size": size
        }));
    }
    
    HttpResponse::Ok().json(serde_json::json!({
        "files": files
    }))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(login)
            .service(contact)
            .service(upload)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

测试表单 #

bash
# URL 编码表单
curl -X POST http://localhost:8080/login \
  -d "username=admin&password=secret"

# 多部分表单
curl -X POST http://localhost:8080/upload \
  -F "file=@/path/to/file.txt"

# 带文本字段的多部分表单
curl -X POST http://localhost:8080/upload \
  -F "description=My file" \
  -F "file=@/path/to/file.txt"

下一步 #

现在你已经掌握了表单数据处理,继续学习 JSON 处理,深入了解 JSON 请求处理!

最后更新:2026-03-29