表单数据 #
表单类型 #
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