How do I send POST requests with form data using Reqwest?
Reqwest is a powerful HTTP client library for Rust that makes sending POST requests with form data straightforward. Whether you need to submit URL-encoded forms, multipart data, or file uploads, Reqwest provides elegant solutions for all common form submission scenarios.
Understanding Form Data Types
Before diving into implementation, it's important to understand the two main types of form data:
- URL-encoded forms (
application/x-www-form-urlencoded
) - The default HTML form encoding - Multipart forms (
multipart/form-data
) - Used for file uploads and binary data
Basic URL-Encoded Form Data
The simplest way to send form data is using URL-encoded format. Reqwest provides the form()
method for this purpose:
use reqwest::Client;
use std::collections::HashMap;
use tokio;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Create form data using HashMap
let mut form_data = HashMap::new();
form_data.insert("username", "john_doe");
form_data.insert("password", "secret123");
form_data.insert("email", "john@example.com");
let response = client
.post("https://httpbin.org/post")
.form(&form_data)
.send()
.await?;
let status = response.status();
let body = response.text().await?;
println!("Status: {}", status);
println!("Response: {}", body);
Ok(())
}
Using Tuples for Form Data
For simpler cases, you can use tuples instead of HashMap:
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let form_data = [
("name", "Alice Smith"),
("age", "30"),
("city", "New York"),
];
let response = client
.post("https://httpbin.org/post")
.form(&form_data)
.send()
.await?;
println!("Status: {}", response.status());
Ok(())
}
Sending Multipart Form Data
For file uploads or when you need to send binary data, use multipart forms:
use reqwest::{Client, multipart};
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Create multipart form
let form = multipart::Form::new()
.text("title", "My Document")
.text("description", "Important file upload")
.text("category", "documents");
let response = client
.post("https://httpbin.org/post")
.multipart(form)
.send()
.await?;
println!("Upload status: {}", response.status());
Ok(())
}
File Upload with Multipart Forms
Here's how to upload files using multipart forms:
use reqwest::{Client, multipart};
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Read file
let file = File::open("document.pdf").await?;
let stream = FramedRead::new(file, BytesCodec::new());
let file_body = reqwest::Body::wrap_stream(stream);
// Create multipart form with file
let form = multipart::Form::new()
.text("user_id", "12345")
.text("upload_type", "document")
.part("file", multipart::Part::stream(file_body)
.file_name("document.pdf")
.mime_str("application/pdf")?);
let response = client
.post("https://api.example.com/upload")
.multipart(form)
.send()
.await?;
if response.status().is_success() {
println!("File uploaded successfully!");
} else {
println!("Upload failed: {}", response.status());
}
Ok(())
}
Advanced Form Handling with Custom Headers
Sometimes you need to add custom headers or authentication:
use reqwest::{Client, header};
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let mut form_data = HashMap::new();
form_data.insert("action", "create_user");
form_data.insert("username", "newuser");
form_data.insert("role", "editor");
let response = client
.post("https://api.example.com/admin/users")
.header(header::AUTHORIZATION, "Bearer your-api-token")
.header(header::USER_AGENT, "MyApp/1.0")
.form(&form_data)
.send()
.await?;
match response.status() {
reqwest::StatusCode::OK => {
let json: serde_json::Value = response.json().await?;
println!("Success: {}", json);
}
reqwest::StatusCode::UNAUTHORIZED => {
println!("Authentication failed");
}
status => {
println!("Request failed with status: {}", status);
}
}
Ok(())
}
Error Handling and Retries
Robust form submission requires proper error handling:
use reqwest::{Client, Error};
use std::collections::HashMap;
use tokio::time::{sleep, Duration};
async fn submit_form_with_retry(
client: &Client,
url: &str,
form_data: &HashMap<&str, &str>,
max_retries: u32,
) -> Result<String, Error> {
for attempt in 0..=max_retries {
match client.post(url).form(form_data).send().await {
Ok(response) => {
if response.status().is_success() {
return response.text().await;
} else if response.status().is_server_error() && attempt < max_retries {
println!("Server error on attempt {}, retrying...", attempt + 1);
sleep(Duration::from_secs(2_u64.pow(attempt))).await;
continue;
} else {
return Err(Error::from(response.error_for_status().unwrap_err()));
}
}
Err(e) if attempt < max_retries => {
println!("Network error on attempt {}, retrying...", attempt + 1);
sleep(Duration::from_secs(2_u64.pow(attempt))).await;
continue;
}
Err(e) => return Err(e),
}
}
unreachable!()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let mut form_data = HashMap::new();
form_data.insert("message", "Hello, world!");
form_data.insert("priority", "high");
match submit_form_with_retry(&client, "https://api.example.com/messages", &form_data, 3).await {
Ok(response) => println!("Form submitted successfully: {}", response),
Err(e) => println!("Failed to submit form: {}", e),
}
Ok(())
}
Working with JSON Form Responses
Many APIs return JSON responses after form submission:
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
struct ApiResponse {
success: bool,
message: String,
data: Option<serde_json::Value>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let mut form_data = HashMap::new();
form_data.insert("email", "user@example.com");
form_data.insert("newsletter", "true");
let response = client
.post("https://api.example.com/subscribe")
.form(&form_data)
.send()
.await?;
let api_response: ApiResponse = response.json().await?;
if api_response.success {
println!("Subscription successful: {}", api_response.message);
} else {
println!("Subscription failed: {}", api_response.message);
}
Ok(())
}
Best Practices for Form Submissions
1. Connection Pooling
Use a single client instance for multiple requests to benefit from connection pooling:
use reqwest::Client;
use std::sync::Arc;
#[derive(Clone)]
struct ApiClient {
client: Arc<Client>,
base_url: String,
}
impl ApiClient {
fn new(base_url: String) -> Self {
Self {
client: Arc::new(Client::new()),
base_url,
}
}
async fn submit_contact_form(
&self,
name: &str,
email: &str,
message: &str,
) -> Result<(), reqwest::Error> {
let form_data = [
("name", name),
("email", email),
("message", message),
];
let url = format!("{}/contact", self.base_url);
let response = self.client
.post(&url)
.form(&form_data)
.send()
.await?;
response.error_for_status()?;
Ok(())
}
}
2. Timeout Configuration
Always set appropriate timeouts for form submissions:
use reqwest::{Client, ClientBuilder};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new()
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.build()?;
// Your form submission code here
Ok(())
}
Integration with Web Scraping Workflows
When building web scraping applications, form submissions often need to be combined with other HTTP operations. While Reqwest excels at HTTP client operations, you might also need to handle authentication workflows or monitor network requests in more complex scenarios involving JavaScript-heavy sites.
Conclusion
Reqwest provides excellent support for sending POST requests with form data in Rust applications. Whether you're working with simple URL-encoded forms or complex multipart uploads, the library offers intuitive APIs that handle the underlying HTTP complexities. Remember to implement proper error handling, use connection pooling for better performance, and set appropriate timeouts for production applications.
The examples above demonstrate the most common form submission patterns you'll encounter in web scraping and API integration scenarios. By mastering these techniques, you'll be well-equipped to handle any form-based interactions in your Rust applications.