HTTP client in Rust: advanced techniques and best practices

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: why an advanced HTTP client in Rust matters

Rust has earned a reputation as a language for systems where performance and reliability are important. However, when writing an HTTP client in Rust, many developers stop at the basic examples from the documentation. In real projects — microservices, parsers, CI/CD tools — a simple reqwest::get() call doesn't hold up. Crashes due to network errors, hangs on slow responses, connection leaks — all these are consequences of a superficial approach.

In this article, we will cover the advanced level of working with an HTTP client in Rust. You will learn how to properly handle errors, configure timeouts, implement a retry mechanism, and manage TLS. All examples are written in Rust using the reqwest and tokio crates — the standard stack for asynchronous HTTP requests.



1. Error handling in an HTTP client in Rust

Any network request can fail: connection break, DNS error, incorrect response status. In Rust, the powerful Result and Error type system is used for this. An advanced approach is not just to check the status, but to log and classify errors.



Structured error handling

Let's create a custom error type for the HTTP client:

use std::fmt;use reqwest::StatusCode;

#[derive(Debug)]pub enum HttpClientError { NetworkError(reqwest::Error), BadStatus(StatusCode), Timeout, InvalidUrl(String),}

impl fmt::Display for HttpClientError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { HttpClientError::NetworkError(e) => write!(f, "Network error: {}", e), HttpClientError::BadStatus(s) => write!(f, "Bad status: {}", s), HttpClientError::Timeout => write!(f, "Request timeout"), HttpClientError::InvalidUrl(url) => write!(f, "Invalid URL: {}", url), } }}

impl From<reqwest::Error> for HttpClientError { fn from(e: reqwest::Error) -> Self { if e.is_timeout() { HttpClientError::Timeout } else if e.is_connect() { HttpClientError::NetworkError(e) } else { HttpClientError::NetworkError(e) } }}


Now the request function returns a clearly typed error:

async fn fetch_data(url: &str) -> Result<String, HttpClientError> {    if !url.starts_with("http") {        return Err(HttpClientError::InvalidUrl(url.to_string()));    }

let response = reqwest::get(url).await?;

match response.status() { StatusCode::OK => { let body = response.text().await?; Ok(body) } status => Err(HttpClientError::BadStatus(status)), }}


This approach allows the calling code to know exactly what went wrong and make the right decisions: retry the request on a network error, but not on a 400 Bad Request.



2. Timeouts and connection management

Without timeouts, your HTTP client in Rust can hang for minutes. Advanced configuration includes three levels: connection timeout, read timeout, and a total request timeout.



Configuring a client with timeouts

use std::time::Duration;use reqwest::Client;

async fn create_advanced_client() -> Result<Client, reqwest::Error> { let client = Client::builder() .timeout(Duration::from_secs(30)) // total timeout .connect_timeout(Duration::from_secs(5)) // connection timeout .pool_max_idle_per_host(10) // max idle connections .tcp_keepalive(Duration::from_secs(60)) // keepalive .build()?;

Ok(client)}


Now we use the client with timeouts in real

Blogs

Book Recommendations