Table of contents

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:

  1. URL-encoded forms (application/x-www-form-urlencoded) - The default HTML form encoding
  2. 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.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon