ureq
Lightweight HTTP client for Rust with minimal dependencies. Synchronous-only design providing simple and understandable API. Prioritizes compile time, optimized for use cases not requiring async. Efficiently provides basic features like JSON processing, basic authentication, and cookie management.
GitHub Overview
Topics
Star History
Library
ureq
Overview
ureq is a lightweight HTTP client library for Rust developed as a "simple and safe HTTP client." It's a library that emphasizes "safety and understandability through Pure Rust, excellent for HTTP API integration," using blocking I/O instead of asynchronous I/O to keep the API simple and minimize dependencies. Providing basic features like JSON, cookies, proxies, HTTPS, and charset decoding, it's the optimal HTTP client for developers who want to minimize impact on compile time.
Details
ureq 2025 edition has established itself as a Rust HTTP client designed with "lightness and simplicity" as top priorities. Implemented in Pure Rust without directly using unsafe code, it leverages zero-cost abstractions to balance performance and safety. The synchronous processing-based design avoids the complexity of async/await while providing an intuitive and understandable API. Flexible feature selection through crate feature flags allows enabling only the minimum necessary features to optimize binary size and compile time.
Key Features
- Pure Rust Implementation: Safety and understandability by avoiding unsafe usage
- Lightweight Design: Minimal dependencies and reduced compile time
- Simple API: Intuitive and understandable blocking I/O based
- Agent Functionality: Efficient communication through connection pools and cookie stores
- TLS Flexibility: Support for both rustls and native-tls with provider selection
- Rich Feature Flags: Selective enabling of JSON, compression, proxy, etc.
Pros and Cons
Pros
- Lightweight with short compile times improving development efficiency
- High safety and memory safety through Pure Rust implementation
- Low learning cost due to simple and intuitive API
- Ability to select only necessary features through rich feature flags
- Straightforward processing avoiding async/await complexity
- Flexible support for proxy and TLS configurations in enterprise environments
Cons
- Only supports synchronous processing, no asynchronous processing support
- Unsuitable for high-load parallel requests
- Difficult integration with tokio ecosystem
- Performance constraints in scenarios requiring large-scale parallel processing
- Inappropriate for responsive GUI applications
- May have lower throughput compared to asynchronous libraries
Reference Pages
Code Examples
Installation and Basic Setup
# Cargo.toml - Basic configuration
[dependencies]
ureq = "3.0"
# Installation with feature flags
[dependencies]
ureq = { version = "3.0", features = ["json", "cookies", "gzip"] }
# TLS provider selection
[dependencies]
ureq = { version = "3.0", features = ["native-tls"] }
# SOCKS proxy support
[dependencies]
ureq = { version = "3.0", features = ["socks-proxy"] }
# Minimal configuration (JSON, Cookies disabled)
[dependencies]
ureq = { version = "3.0", default-features = false }
// Basic imports
use ureq;
use std::collections::HashMap;
// For error handling
use ureq::{Error, Response};
// When JSON feature is enabled
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
fn main() {
println!("ureq HTTP client started");
}
Basic Requests (GET/POST/PUT/DELETE)
use ureq;
use std::io::Read;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Basic GET request
let response = ureq::get("https://api.example.com/users")
.call()?;
println!("Status: {}", response.status());
println!("Headers: {:?}", response.headers_names());
let body: String = response.into_string()?;
println!("Response: {}", body);
// GET request with headers
let response = ureq::get("https://api.example.com/data")
.header("User-Agent", "MyApp/1.0")
.header("Accept", "application/json")
.header("Authorization", "Bearer your-token")
.call()?;
// Check response headers
if let Some(content_type) = response.header("content-type") {
println!("Content-Type: {}", content_type);
}
// GET request with parameters (query string)
let response = ureq::get("https://api.example.com/users")
.query("page", "1")
.query("limit", "10")
.query("sort", "created_at")
.call()?;
println!("Request URL: {}", response.get_url());
// POST request (sending JSON) - when JSON feature is enabled
#[cfg(feature = "json")]
{
use serde_json::json;
let user_data = json!({
"name": "John Doe",
"email": "[email protected]",
"age": 30
});
let response = ureq::post("https://api.example.com/users")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer your-token")
.send_json(&user_data)?;
if response.status() == 201 {
let created_user: serde_json::Value = response.into_json()?;
println!("User created: ID={}", created_user["id"]);
}
}
// POST request (sending form data)
let form_data = [
("username", "testuser"),
("password", "secret123"),
];
let response = ureq::post("https://api.example.com/login")
.send_form(&form_data)?;
// POST request (sending string)
let response = ureq::post("https://api.example.com/webhook")
.header("Content-Type", "text/plain")
.send_string("Hello, webhook!")?;
// PUT request (data update)
#[cfg(feature = "json")]
{
let updated_data = json!({
"name": "Jane Doe",
"email": "[email protected]"
});
let response = ureq::put("https://api.example.com/users/123")
.header("Authorization", "Bearer your-token")
.send_json(&updated_data)?;
println!("Update status: {}", response.status());
}
// DELETE request
let response = ureq::delete("https://api.example.com/users/123")
.header("Authorization", "Bearer your-token")
.call()?;
if response.status() == 204 {
println!("Deletion completed");
}
// HEAD request (header information only)
let response = ureq::head("https://api.example.com/users/123")
.call()?;
if let Some(content_length) = response.header("content-length") {
println!("Content-Length: {}", content_length);
}
Ok(())
}
Advanced Configuration and Error Handling (Agent, Timeout, Proxy, etc.)
use ureq::{Agent, AgentBuilder, Error};
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create Agent (with connection pool and cookie store enabled)
let agent = AgentBuilder::new()
.timeout_read(Duration::from_secs(30))
.timeout_write(Duration::from_secs(30))
.timeout_connect(Duration::from_secs(10))
.user_agent("MyApp/1.0 (ureq)")
.build();
// Execute request with Agent
let response = agent
.get("https://api.example.com/data")
.header("Accept", "application/json")
.call()?;
// Using cookie feature when enabled
#[cfg(feature = "cookies")]
{
// Cookies are automatically saved and sent
let _login_response = agent
.post("https://api.example.com/login")
.send_form(&[("user", "testuser"), ("pass", "secret")])?;
// Cookies are automatically sent in subsequent requests
let protected_response = agent
.get("https://api.example.com/protected")
.call()?;
}
// Proxy configuration
#[cfg(feature = "socks-proxy")]
{
let proxy_agent = AgentBuilder::new()
.proxy("socks5://127.0.0.1:1080")
.build();
let response = proxy_agent
.get("https://api.example.com/via-proxy")
.call()?;
}
// Custom timeout configuration
let timeout_agent = AgentBuilder::new()
.timeout(Duration::from_secs(60)) // Overall timeout
.timeout_connect(Duration::from_secs(5)) // Connection timeout
.timeout_read(Duration::from_secs(30)) // Read timeout
.timeout_write(Duration::from_secs(30)) // Write timeout
.build();
// Custom TLS configuration
use ureq::tls::{TlsConfig, RootCerts};
let tls_agent = AgentBuilder::new()
.tls_config(
TlsConfig::builder()
.root_certs(RootCerts::PlatformVerifier)
.build()
)
.build();
// Detailed error handling
match ureq::get("https://api.example.com/might-fail").call() {
Ok(response) => {
println!("Success: Status {}", response.status());
// Processing by response type
match response.status() {
200 => {
let body = response.into_string()?;
println!("Data: {}", body);
},
404 => println!("Resource not found"),
_ => println!("Other status: {}", response.status()),
}
},
Err(Error::Status(code, response)) => {
println!("HTTP error: {} - {}", code, response.status_text());
// Check error response content
if let Ok(error_body) = response.into_string() {
println!("Error details: {}", error_body);
}
},
Err(Error::Transport(transport_error)) => {
println!("Transport error: {}", transport_error);
},
Err(e) => {
println!("Other error: {}", e);
}
}
Ok(())
}
// Retry functionality implementation
fn request_with_retry(
url: &str,
max_retries: usize,
) -> Result<ureq::Response, ureq::Error> {
let agent = ureq::agent();
for attempt in 0..=max_retries {
match agent.get(url).call() {
Ok(response) => return Ok(response),
Err(Error::Status(code, _)) if code >= 500 && attempt < max_retries => {
println!("Server error {}, retrying... ({}/{})", code, attempt + 1, max_retries);
std::thread::sleep(Duration::from_secs(2_u64.pow(attempt as u32)));
continue;
},
Err(Error::Transport(_)) if attempt < max_retries => {
println!("Transport error, retrying... ({}/{})", attempt + 1, max_retries);
std::thread::sleep(Duration::from_secs(2_u64.pow(attempt as u32)));
continue;
},
Err(e) => return Err(e),
}
}
unreachable!()
}
// Authenticated requests
fn authenticated_request() -> Result<(), Box<dyn std::error::Error>> {
let agent = ureq::agent();
// Bearer authentication
let response = agent
.get("https://api.example.com/protected")
.header("Authorization", "Bearer your-jwt-token")
.call()?;
// Basic authentication
let response = agent
.get("https://api.example.com/basic-auth")
.auth("username", Some("password"))
.call()?;
Ok(())
}
JSON Processing and Data Serialization
use ureq;
use serde::{Deserialize, Serialize};
use serde_json;
#[derive(Serialize, Deserialize, Debug)]
struct User {
id: Option<u32>,
name: String,
email: String,
age: u32,
}
#[derive(Deserialize, Debug)]
struct ApiResponse<T> {
success: bool,
data: T,
message: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let agent = ureq::agent();
// Automatic deserialization of JSON responses
let users_response = agent
.get("https://api.example.com/users")
.header("Accept", "application/json")
.call()?;
#[cfg(feature = "json")]
{
// Direct deserialization to struct
let users: Vec<User> = users_response.into_json()?;
for user in &users {
println!("User: {} ({})", user.name, user.email);
}
// Deserialization of nested structures
let api_response: ApiResponse<Vec<User>> = agent
.get("https://api.example.com/users-wrapped")
.call()?
.into_json()?;
if api_response.success {
println!("API success: {}", api_response.message);
for user in api_response.data {
println!(" - {}: {}", user.name, user.email);
}
}
// Creating a new user
let new_user = User {
id: None,
name: "New User".to_string(),
email: "[email protected]".to_string(),
age: 25,
};
let created_user: User = agent
.post("https://api.example.com/users")
.header("Content-Type", "application/json")
.send_json(&new_user)?
.into_json()?;
println!("Created user: {:?}", created_user);
// Partial JSON processing
let partial_update = serde_json::json!({
"age": 26
});
let updated_user: User = agent
.patch(&format!("https://api.example.com/users/{}", created_user.id.unwrap()))
.send_json(&partial_update)?
.into_json()?;
println!("Updated user: {:?}", updated_user);
}
// Manual JSON parsing (available even without feature = "json")
let response = agent
.get("https://api.example.com/data")
.call()?;
let json_string = response.into_string()?;
let parsed_data: serde_json::Value = serde_json::from_str(&json_string)?;
if let Some(name) = parsed_data["name"].as_str() {
println!("Name: {}", name);
}
Ok(())
}
// Error response handling
#[derive(Deserialize, Debug)]
struct ErrorResponse {
error: String,
code: u32,
details: Option<String>,
}
fn handle_api_errors() -> Result<(), Box<dyn std::error::Error>> {
let agent = ureq::agent();
match agent.get("https://api.example.com/might-error").call() {
Ok(response) => {
#[cfg(feature = "json")]
{
let data: serde_json::Value = response.into_json()?;
println!("Success: {:?}", data);
}
},
Err(ureq::Error::Status(code, response)) => {
println!("HTTP error: {}", code);
#[cfg(feature = "json")]
{
if let Ok(error_detail) = response.into_json::<ErrorResponse>() {
println!("Error details: {:?}", error_detail);
} else {
println!("Failed to parse error response");
}
}
},
Err(e) => {
println!("Request error: {}", e);
}
}
Ok(())
}
File Operations and Streaming Processing
use ureq;
use std::fs::File;
use std::io::{Write, Read, copy};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let agent = ureq::agent();
// File download
let response = agent
.get("https://api.example.com/files/document.pdf")
.call()?;
let mut file = File::create("downloaded_document.pdf")?;
let mut reader = response.into_reader();
copy(&mut reader, &mut file)?;
println!("File download completed");
// Streaming download of large files
let response = agent
.get("https://api.example.com/files/large-file.zip")
.call()?;
let mut file = File::create("large_file.zip")?;
let mut reader = response.into_reader();
let mut buffer = [0; 8192];
let mut total_downloaded = 0;
loop {
let bytes_read = reader.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])?;
total_downloaded += bytes_read;
// Progress display
if total_downloaded % (1024 * 1024) == 0 {
println!("Downloaded: {} MB", total_downloaded / (1024 * 1024));
}
}
println!("Large file download completed: {} bytes", total_downloaded);
// File upload
let file_content = std::fs::read("upload_file.txt")?;
let response = agent
.post("https://api.example.com/upload")
.header("Content-Type", "text/plain")
.send_bytes(&file_content)?;
println!("File upload: {}", response.status());
// File upload with multipart form data
// Note: ureq doesn't directly support multipart,
// so you need to use it with crates like multipart
Ok(())
}
// Download with response size limit
fn limited_download(url: &str, max_size: usize) -> Result<String, Box<dyn std::error::Error>> {
let agent = ureq::agent();
let response = agent.get(url).call()?;
let mut reader = response.into_reader();
let mut buffer = Vec::new();
let mut temp_buffer = [0; 1024];
loop {
let bytes_read = reader.read(&mut temp_buffer)?;
if bytes_read == 0 {
break;
}
if buffer.len() + bytes_read > max_size {
return Err("File size exceeds limit".into());
}
buffer.extend_from_slice(&temp_buffer[..bytes_read]);
}
Ok(String::from_utf8(buffer)?)
}
// Streaming JSON processing (line-delimited JSON)
fn process_streaming_json(url: &str) -> Result<(), Box<dyn std::error::Error>> {
let agent = ureq::agent();
let response = agent.get(url).call()?;
let reader = response.into_reader();
let mut lines = std::io::BufRead::lines(std::io::BufReader::new(reader));
for line in lines {
let line = line?;
if !line.trim().is_empty() {
match serde_json::from_str::<serde_json::Value>(&line) {
Ok(json_data) => {
println!("Received: {}", json_data);
},
Err(e) => {
println!("JSON parse error: {} - Line: {}", e, line);
}
}
}
}
Ok(())
}
Practical Usage Examples and Best Practices
use ureq::{Agent, AgentBuilder};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use std::sync::Arc;
// REST API client struct
#[derive(Clone)]
pub struct ApiClient {
agent: Agent,
base_url: String,
api_key: Option<String>,
}
impl ApiClient {
pub fn new(base_url: &str) -> Self {
let agent = AgentBuilder::new()
.timeout(Duration::from_secs(30))
.user_agent("ApiClient/1.0")
.build();
Self {
agent,
base_url: base_url.to_string(),
api_key: None,
}
}
pub fn with_api_key(mut self, api_key: &str) -> Self {
self.api_key = Some(api_key.to_string());
self
}
fn build_url(&self, path: &str) -> String {
format!("{}/{}", self.base_url.trim_end_matches('/'), path.trim_start_matches('/'))
}
fn prepare_request(&self, request: ureq::Request) -> ureq::Request {
let mut req = request.header("Accept", "application/json");
if let Some(ref api_key) = self.api_key {
req = req.header("Authorization", &format!("Bearer {}", api_key));
}
req
}
pub fn get(&self, path: &str) -> Result<ureq::Response, ureq::Error> {
let url = self.build_url(path);
let request = self.agent.get(&url);
self.prepare_request(request).call()
}
#[cfg(feature = "json")]
pub fn post_json<T: Serialize>(&self, path: &str, data: &T) -> Result<ureq::Response, ureq::Error> {
let url = self.build_url(path);
let request = self.agent.post(&url);
self.prepare_request(request)
.header("Content-Type", "application/json")
.send_json(data)
}
#[cfg(feature = "json")]
pub fn put_json<T: Serialize>(&self, path: &str, data: &T) -> Result<ureq::Response, ureq::Error> {
let url = self.build_url(path);
let request = self.agent.put(&url);
self.prepare_request(request)
.header("Content-Type", "application/json")
.send_json(data)
}
pub fn delete(&self, path: &str) -> Result<ureq::Response, ureq::Error> {
let url = self.build_url(path);
let request = self.agent.delete(&url);
self.prepare_request(request).call()
}
}
// Usage example
#[cfg(feature = "json")]
fn api_client_example() -> Result<(), Box<dyn std::error::Error>> {
let client = ApiClient::new("https://api.example.com/v1")
.with_api_key("your-api-key");
// GET request
let users_response = client.get("users")?;
let users: Vec<User> = users_response.into_json()?;
println!("Number of users: {}", users.len());
// POST request
let new_user = User {
id: None,
name: "New User".to_string(),
email: "[email protected]".to_string(),
age: 30,
};
let created_response = client.post_json("users", &new_user)?;
let created_user: User = created_response.into_json()?;
println!("Created user: {:?}", created_user);
Ok(())
}
// Configuration management and best practices
pub struct HttpConfig {
pub timeout: Duration,
pub user_agent: String,
pub max_redirects: usize,
}
impl Default for HttpConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
user_agent: "MyApp/1.0".to_string(),
max_redirects: 5,
}
}
}
// HTTP client using global configuration
pub fn create_configured_agent(config: &HttpConfig) -> Agent {
AgentBuilder::new()
.timeout(config.timeout)
.user_agent(&config.user_agent)
.redirects(config.max_redirects)
.build()
}
// Error handling best practices
#[derive(Debug)]
pub enum ApiError {
NetworkError(ureq::Error),
ParseError(String),
BusinessLogicError(String),
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiError::NetworkError(e) => write!(f, "Network error: {}", e),
ApiError::ParseError(e) => write!(f, "Parse error: {}", e),
ApiError::BusinessLogicError(e) => write!(f, "Business logic error: {}", e),
}
}
}
impl std::error::Error for ApiError {}
impl From<ureq::Error> for ApiError {
fn from(error: ureq::Error) -> Self {
ApiError::NetworkError(error)
}
}
// Robust API call function
pub fn robust_api_call(url: &str) -> Result<serde_json::Value, ApiError> {
let agent = ureq::agent();
match agent.get(url).call() {
Ok(response) => {
#[cfg(feature = "json")]
{
response.into_json()
.map_err(|e| ApiError::ParseError(e.to_string()))
}
#[cfg(not(feature = "json"))]
{
Err(ApiError::ParseError("JSON feature is disabled".to_string()))
}
},
Err(ureq::Error::Status(code, response)) => {
match code {
400..=499 => Err(ApiError::BusinessLogicError(
format!("Client error: {}", code)
)),
500..=599 => Err(ApiError::NetworkError(
ureq::Error::Status(code, response)
)),
_ => Err(ApiError::NetworkError(
ureq::Error::Status(code, response)
)),
}
},
Err(e) => Err(ApiError::NetworkError(e)),
}
}
// Main function with integrated usage example
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Basic usage
let response = ureq::get("https://api.example.com/status").call()?;
println!("Service status: {}", response.status());
// Using configured agent
let config = HttpConfig::default();
let agent = create_configured_agent(&config);
let response = agent
.get("https://api.example.com/data")
.header("Accept", "application/json")
.call()?;
// Robust API call
match robust_api_call("https://api.example.com/users") {
Ok(data) => println!("Data retrieval successful: {:?}", data),
Err(e) => println!("Error: {}", e),
}
#[cfg(feature = "json")]
{
// Using API client
api_client_example()?;
}
Ok(())
}