What is the difference between reqwest::Client::execute and get for controlling request construction?
reqwest::get is a convenience function that creates a new client for each request, while Client::execute gives you control over the request object and uses a shared client with connection pooling. The get function is ideal for simple one-off requests where you don't need configuration. Client::execute requires building a Request object explicitly but enables reuse of the client, custom configuration, and fine-grained control over headers, body, and method. Understanding this distinction helps you choose between simplicity and control in HTTP client code.
The Convenience Function: reqwest::get
use reqwest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// reqwest::get is a convenience function
// It creates a new Client internally for each call
let response = reqwest::get("https://httpbin.org/get").await?;
println!("Status: {}", response.status());
let body = response.text().await?;
println!("Body: {}", body);
// Key characteristics:
// - Creates a new Client internally
// - No configuration options
// - Simple one-off requests
// - No connection pooling between calls
// - Returns Response directly
Ok(())
}reqwest::get abstracts away client creation for simple use cases.
The Client::execute Approach
use reqwest::{Client, Request};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a reusable client with configuration
let client = Client::builder()
.timeout(std::time::Duration::from_secs(10))
.user_agent("my-app/1.0")
.build()?;
// Build a Request object
let request: Request = client
.get("https://httpbin.org/get")
.header("X-Custom-Header", "value")
.build()?;
// Execute the pre-built request
let response = client.execute(request).await?;
println!("Status: {}", response.status());
// Key characteristics:
// - Client is reused across requests
// - Connection pooling enabled
// - Full configuration control
// - Request built separately from execution
// - Can inspect/modify Request before execution
Ok(())
}Client::execute takes ownership of a Request object you've constructed.
RequestBuilder vs Direct execute
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Approach 1: RequestBuilder (most common)
// .send() implicitly builds and executes
let response1 = client
.get("https://httpbin.org/get")
.header("Accept", "application/json")
.send()
.await?;
// Approach 2: Build then execute (separated)
let request = client
.get("https://httpbin.org/get")
.header("Accept", "application/json")
.build()?;
// Can inspect or modify request here
println!("Request URL: {:?}", request.url());
println!("Request method: {}", request.method());
// Then execute
let response2 = client.execute(request).await?;
// Approach 2 gives you:
// - Ability to inspect Request before sending
// - Ability to modify Request
// - Ability to retry with same Request
// - Decoupling of request construction from execution
Ok(())
}RequestBuilder::send() combines build and execute; execute separates them.
Connection Pooling Implications
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Using reqwest::get - no connection pooling
// Each call creates a new Client internally
// These are SEPARATE clients with SEPARATE connection pools
let resp1 = reqwest::get("https://httpbin.org/get").await?;
let resp2 = reqwest::get("https://httpbin.org/get").await?;
// Each establishes a new connection (no reuse)
// Using Client::execute - connection pooling works
let client = Client::new();
let req1 = client.get("https://httpbin.org/get").build()?;
let req2 = client.get("https://httpbin.org/get").build()?;
// These share the same connection pool
let resp1 = client.execute(req1).await?;
let resp2 = client.execute(req2).await?;
// Second request may reuse connection from first
// Connection pooling benefits:
// - Reduced latency (no new TCP handshake)
// - Reduced server load
// - Better throughput for repeated requests
// execute with shared client is more efficient for multiple requests
Ok(())
}reqwest::get can't pool connections; Client::execute with a shared client does.
Reusing Request Objects
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Build a request once
let base_request = client
.post("https://httpbin.org/post")
.header("Content-Type", "application/json")
.body(r#"{"key": "value"}"#);
// Note: build() consumes the builder
let request = base_request.build()?;
// execute() takes ownership of the Request
let response = client.execute(request).await?;
println!("First attempt: {}", response.status());
// Can't reuse request - it was consumed
// let response2 = client.execute(request).await?; // ERROR: request moved
// For retries, you'd need to rebuild:
let request2 = client
.post("https://httpbin.org/post")
.header("Content-Type", "application/json")
.body(r#"{"key": "value"}"#)
.build()?;
let response2 = client.execute(request2).await?;
println!("Second attempt: {}", response2.status());
// Or use RequestBuilder::try_clone() for retries
let builder = client
.post("https://httpbin.org/post")
.header("Content-Type", "application/json")
.body(r#"{"key": "value"}"#);
// Clone before building (requires body to be clonable)
let request3 = builder.try_clone().unwrap().build()?;
let request4 = builder.try_clone().unwrap().build()?;
let response3 = client.execute(request3).await?;
let response4 = client.execute(request4).await?;
Ok(())
}execute takes ownership of the Request; clone builders for retries.
When to Use Each Approach
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Use reqwest::get when:
// 1. Simple one-off requests
let _ = reqwest::get("https://api.example.com/status").await?;
// 2. No custom configuration needed
// 3. No connection pooling needed
// 4. Quick scripts or prototypes
// Use Client::execute when:
// 1. Multiple requests to same host
let client = Client::new();
for i in 0..10 {
let req = client
.get(&format!("https://api.example.com/item/{}", i))
.build()?;
let _ = client.execute(req).await?;
}
// 2. Need to inspect request before sending
let req = client
.post("https://api.example.com/data")
.header("Authorization", "Bearer token")
.body("data")
.build()?;
println!("About to send to: {}", req.url());
let _ = client.execute(req).await?;
// 3. Need custom client configuration
let configured_client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.connect_timeout(std::time::Duration::from_secs(5))
.max_connections_per_host(10)
.build()?;
let req = configured_client.get("https://api.example.com").build()?;
let _ = configured_client.execute(req).await?;
// 4. Need to separate request construction from execution
fn build_request(client: &Client, data: &str) -> reqwest::Result<Request> {
client
.post("https://api.example.com/submit")
.json(&data)
.build()
}
async fn send_request(client: &Client, request: Request) -> reqwest::Result<()> {
let response = client.execute(request).await?;
println!("Response: {}", response.status());
Ok(())
}
let req = build_request(&client, "test data")?;
send_request(&client, req).await?;
Ok(())
}Choose get for simplicity, execute for control and efficiency.
Request Inspection and Modification
use reqwest::{Client, Method};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Build request with full control
let mut request = client
.request(Method::POST, "https://httpbin.org/post")
.header("X-Request-Id", "12345")
.json(&serde_json::json!({"key": "value"}))
.build()?;
// Inspect before sending
println!("Method: {}", request.method());
println!("URL: {}", request.url());
println!("Headers: {:?}", request.headers());
// Note: body() consumes the body, so be careful with inspection
// For debugging without consuming:
if let Some(content_length) = request.headers().get("content-length") {
println!("Content-Length: {:?}", content_length);
}
// Can modify headers
request.headers_mut().insert(
"X-Custom-Header",
"modified-value".parse()?
);
// Execute the potentially modified request
let response = client.execute(request).await?;
println!("Response status: {}", response.status());
// This is useful for:
// - Debugging/logging requests before sending
// - Adding tracing headers
// - Conditional header modification
// - Testing with mock servers
Ok(())
}execute allows inspection and modification between build and send.
Client Configuration Options
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Client::execute supports configuration that get doesn't
let client = Client::builder()
// Timeout configuration
.timeout(std::time::Duration::from_secs(30))
.connect_timeout(std::time::Duration::from_secs(5))
.read_timeout(std::time::Duration::from_secs(10))
// Connection pooling
.pool_max_idle_per_host(10)
.pool_idle_timeout(std::time::Duration::from_secs(60))
// TLS configuration
.danger_accept_invalid_certs(false)
.tls_built_in_root_certs(true)
// Headers for all requests
.user_agent("my-app/1.0")
.default_headers({
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("X-App-Version", "1.0".parse()?);
headers
})
// Redirect behavior
.redirect(reqwest::redirect::Policy::limited(5))
// Cookie store
.cookie_store(true)
.build()?;
// All requests through this client use these defaults
let request = client
.get("https://httpbin.org/get")
.build()?;
// Request includes:
// - User-Agent: my-app/1.0
// - X-App-Version: 1.0
// - 30 second timeout
// - Connection pooling
let response = client.execute(request).await?;
println!("Response: {}", response.status());
// reqwest::get can't use any of this configuration
Ok(())
}Client supports extensive configuration that reqwest::get cannot use.
Error Handling Differences
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// reqwest::get returns Result<Response, Error>
// Errors can be:
// - Network errors
// - DNS resolution failures
// - Timeout errors
// - TLS errors
// Client::execute has same error types
// But RequestBuilder::build() can ALSO error
let client = Client::new();
// Build errors are separate from execution errors
let build_result = client
.get("not a valid url")
.build();
match build_result {
Ok(request) => {
let response = client.execute(request).await?;
println!("Response: {}", response.status());
}
Err(e) => {
// URL parsing error, not network error
println!("Failed to build request: {}", e);
}
}
// With get, URL errors are also caught in the Result
// But you can't separate build from execute
// For better error handling, use execute:
let url = "https://httpbin.org/get";
let request = client.get(url).build()?;
// Now you have a valid Request
// Execution errors are separate from construction errors
match client.execute(request).await {
Ok(response) => {
println!("Success: {}", response.status());
}
Err(e) => {
if e.is_timeout() {
println!("Request timed out");
} else if e.is_connect() {
println!("Connection failed");
} else {
println!("Error: {}", e);
}
}
}
Ok(())
}execute separates construction errors from execution errors.
Comparison Summary
use reqwest::{Client, Request};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Quick comparison:
// reqwest::get
// - Creates new Client each call
// - No configuration
// - No connection pooling between calls
// - Simple API: just URL string
// - Returns Result<Response>
// - Best for: one-off requests, scripts, tests
// Client::execute
// - Uses shared Client
// - Full configuration
// - Connection pooling
// - Requires building Request
// - Takes ownership of Request
// - Best for: production apps, multiple requests, control
// RequestBuilder::send
// - Uses shared Client
// - Configuration available
// - Connection pooling
// - Combines build + execute
// - Best for: common case, simpler than execute
// Example showing all three for same request:
// 1. get (simplest)
let resp1 = reqwest::get("https://httpbin.org/get").await?;
// 2. RequestBuilder::send (common)
let client = Client::new();
let resp2 = client.get("https://httpbin.org/get").send().await?;
// 3. Client::execute (most control)
let client = Client::new();
let request = client.get("https://httpbin.org/get").build()?;
// ... can inspect/modify request here
let resp3 = client.execute(request).await?;
println!("All succeeded");
Ok(())
}Use the simplest approach that meets your needs.
Synthesis
Quick reference:
use reqwest::{Client, Request, Response};
// reqwest::get - convenience function
async fn simple_get() -> reqwest::Result<Response> {
// Creates Client internally, one-off request
// No configuration, no connection pooling
reqwest::get("https://api.example.com/data").await
}
// Client::execute - full control
async fn controlled_execute(client: &Client) -> reqwest::Result<Response> {
// Build Request object
let request: Request = client
.get("https://api.example.com/data")
.header("X-Custom", "value")
.timeout(std::time::Duration::from_secs(10))
.build()?;
// Inspect/modify request before sending
println!("Sending to: {}", request.url());
// Execute with shared client (connection pooling)
client.execute(request).await
}
// Key differences:
// - get: creates Client each time (no pooling)
// - execute: uses shared Client (pooling works)
// - get: no configuration
// - execute: full Client configuration available
// - get: URL only
// - execute: takes Request object (can inspect/modify)
// - get: simple, one-line
// - execute: more verbose, more control
// When to use get:
// - One-off requests
// - Scripts/prototypes
// - No configuration needed
// - No connection reuse needed
// When to use execute:
// - Multiple requests (connection pooling)
// - Custom client configuration
// - Need to inspect Request before sending
// - Separation of concerns (build vs execute)
// - Production applicationsKey insight: reqwest::get is a convenience wrapper that creates a new Client internally, making it suitable for simple one-off requests but inefficient for multiple requests due to lack of connection pooling. Client::execute requires explicit request construction but enables connection pooling, client configuration, and request inspection. For most production applications, create a Client once and use either RequestBuilder::send() for simplicity or Client::execute for maximum control. The separation of build and execute allows for debugging, logging, and modification of requests before they're sent.
