Editor’s note: This Rust article was last updated on 21 December 2022 to update code and include less well-known but useful HTTP clients, such as Actix Web Client, rustify, and tokio-curl. Check out this article for a tutorial on making HTTP requests with the reqwest library.
It’s impossible to overstate the importance of HTTP clients, which is why you should do your homework when choosing the right HTTP client for your task at hand. Depending on the tech stack your project is built on or the number of HTTP calls you have to make, certain libraries will suit you well while others could be more trouble than they’re worth.
In this guide, we’ll walk through the main features of six Rust HTTP clients:
We’ll also demonstrate how to make GET and POST requests with each library. Let’s get started!
curl-rust offers libcurl bindings for Rust, meaning it includes an interface for the C-based HTTP library. In other words, you can use the functionality of libcurl with an additional layer of security provided by Rust.
If you want to completely remain in the realm of Rust and avoid the problems that C-based curl can produce, you may want to opt for one of the libraries that completely rely on Rust. Importantly, curl-rust does not offer any support for asynchronous requests, meaning it’s slower and more resource-heavy than any of its asynchronous counterparts.
My advice is to only use curl-rust if you actually need to use curl — for instance, if you’re working on a project that depends on a diverse tech stack and has already integrated curl. curl-rust comes packed with functionalities you don’t always need, like support for transfer protocols such as Telnet, SMTP, FTP, IMAP, or LDAP. There are more lightweight solutions out there that provide more tailored features for today’s web needs.
Here’s an example of a POST request using curl-rust. Notice the more complex syntax:
use std::io::Read; use curl::easy::Easy; fn main() { let mut data = "this is the body".as_bytes(); let mut easy = Easy::new(); easy.url("http://www.example.com/upload").unwrap(); easy.post(true).unwrap(); easy.post_field_size(data.len() as u64).unwrap(); let mut transfer = easy.transfer(); transfer.read_function(|buf| { Ok(data.read(buf).unwrap_or(0)) }).unwrap(); transfer.perform().unwrap(); }
curl-rust’s popularity stats are as follows:
When the high-level libraries don’t offer all the functionalities you’re looking for, it might be a good idea to look into the hyper library. It’s relatively low-level, so it’s perfect for when you need more advanced functionalities.
Several of the libraries we’ll discuss in this article are based on hyper. It’s asynchronous by design and offers both client-side and server-side APIs. It’s a solid, tried-and-true library that enjoys extensive production use.
If you don’t need any of the advanced features hyper offers, it may be worth checking out a more high-level library based on hyper. hyper is meant to be a building block for other libraries and applications. If you want a more convenient HTTP client, hyper recommends checking out reqwest because it’s built on top of hyper (more on this later).
Moreover, hyper hosts a pretty active community of contributors and even runs a Discord server. The official website offers several guides to help you get started quickly with running an HTTP client or server. More details can be found in the API reference — e.g., how the Body
object is implemented.
Here’s how to make a GET request using reqwest. First, create a new client object, then pass it a URL from which to retrieve data. Next, use the new await
syntax Rust supports to await the response:
use hyper::Client; let client = Client::new(); // Parse an `http::Uri`... let uri = "http://httpbin.org/ip".parse()?; // Await the response... let resp = client.get(uri).await?; println!("Response: {}", resp.status());
Here’s an overview of hyper’s advantages:
If you’re keeping score at home:
If you’re looking for a more high-level HTTP client, reqwest may suit your needs. The library offers an HTTP client built on the hyper library.
By default, reqwest includes a client that’s capable of making asynchronous requests. However, it can also be configured so a “blocking” client is used if you only need to make a small number of HTTP calls and don’t want the added complexity of using asynchronous functions. This is an excellent feature.
Features such as HTTP proxies, customizable redirect policies, and support for cookies make reqwest a top choice in terms of convenience. This cookie store can be extremely useful for authentication.
This example from the official documentation shows how to make a GET and POST request. As you can see, the syntax is very straightforward:
// 1. GET let body = reqwest::get("https://www.rust-lang.org") .await? .text() .await?; println!("body = {:?}", body); // 2. POST let client = reqwest::Client::new(); let res = client.post("http://httpbin.org/post") .body("the exact body that is sent") .send() .await?;
To summarize, reqwest offers the following highlighted features:
As for its popularity:
Much like reqwest, Isahc offers a high-level HTTP client with both asynchronous and synchronous methods. However, unlike reqwest, Isahc uses curl under the hood. This can be useful if you want a client that needs to maintain compatibility with curl. It also offers all the usual benefits of using a stable and actively developed, popular library.
Below is an example of a simple synchronous GET request. In particular, the status
, headers
, and text
functions make it very easy to retrieve commonly used information from the response object:
use isahc::prelude::*; fn main() -> Result<(), isahc::Error> { // Send a GET request and wait for the response headers. // Must be `mut` so we can read the response body. let mut response = isahc::get("http://example.org")?; // Print some basic info about the response to standard output. println!("Status: {}", response.status()); println!("Headers: {:#?}", response.headers()); // Read the response body as text into a string and print it. print!("{}", response.text()?); Ok(()) }
Let’s run down Isahc’s most notable features:
The stats below show Isahc’s relative popularity:
Surf is a completely modular client with an asynchronous design. It’s extensible through a powerful middleware system. The default native-client uses curl, but you can also use curl-client.
curl-client uses curl via Isahc as the HTTP server. If you don’t want to use a client built on curl, you can opt to use hyper-client, which uses hyper as the HTTP server.
Here’s a simple middleware example that prints details about each request. As you can see, it uses the classic req
, client
, and next
arguments, which you’ll find in many other HTTP clients. The next
argument allows you to pass the request to another middleware module or request a handler:
use futures::future::BoxFuture; use surf::middleware::{Next, Middleware, Request, Response, HttpClient}; use std::time; /// Log each request's duration #[derive(Debug)] pub struct Logger; impl<C: HttpClient> Middleware<C> for Logger { fn handle<'a>( &'a self, req: Request, client: C, next: Next<'a, C>, ) -> BoxFuture<'a, Result<Response, surf::Exception>> { Box::pin(async move { println!("sending request to {}", req.uri()); let now = time::Instant::now(); let res = next.run(req, client).await?; println!("request completed ({:?})", now.elapsed()); Ok(res) }) } }
The following is an example of a simple GET request with Surf:
#[tokio::main] async fn main() -> Result<(), surf::Error> { let mut res = surf::get("https://httpbin.org/get").await?; println!("{}", res.body_string().await?); Ok(()) }
To run the program, you need these dependencies:
surf = "2.3" tokio = { version = "1", features = ["full"] }
Notable features of Surf include:
As for its popularity:
ureq is a minimal request library, which can be useful if you’re interested in a minimal dependency tree. It’s designed for convenience and offers a blocking API to keep things simple. It doesn’t use any unsafe Rust, which makes it good for developers who want to stick to Safe Rust.
ureq is a particularly excellent choice if you value compile time. It doesn’t use a curl server nor does it offer asynchronous functionality. These are deliberate omissions to keep the library as minimal and lightweight as possible. For this reason, at the time of writing, there are no plans to migrate to asynchronous functionality.
Below is a small GET request example using ureq. Notice the simplicity of setting headers, and making the request:
fn main() -> Result<(), ureq::Error> { let body: String = ureq::get("http://example.com") .set("Example-Header", "header value") .call()? .into_string()?; Ok(()) }
To reiterate, ureq’s main features include:
Finally, let’s look at ureq’s popularity at a glance:
If you’re looking for options beyond the popular libraries for building HTTP clients in Rust, you’re in luck! Here are other less well-known libraries that you might want to consider as you explore your options:
Actix Web Client is a popular library for building HTTP clients in Rust. Known for its speed and efficiency, Actix Web Client is a powerful and pragmatic choice for building web applications in Rust. Below is an example of the library in action:
// create client let mut client = awc::Client::default(); // construct request let req = client.get("http://www.rust-lang.org") .insert_header(("User-Agent", "awc/3.0")); // send request and await response let res = req.send().await?; println!("Response: {:?}", res);
rustify is a lightweight and flexible library for building HTTP clients in Rust. It helps to simplify the process of scaffolding HTTP APIs and offers a range of features for building and consuming HTTP endpoints.
Rustify supports both asynchronous and synchronous clients and allows for custom client implementation using the provided Client trait. Rustify also provides support for serializing requests, deserializing responses, and handling raw request and response data. It includes various helpers for dealing with requests, such as middleware support and response wrapping.
Below is a simple client created with Rustify:
use rustify::{Client, Endpoint}; use rustify_derive::Endpoint; // Defines an API endpoint at /test/path that takes no inputs and returns an // empty response. #[derive(Endpoint)] #[endpoint(path = "test/path")] struct Test {} let endpoint = Test {}; let client = Client::default("http://api.com"); // Configures base address of http://api.com let result = endpoint.exec(&client).await; // Sends GET request to http://api.com/test/path assert!(result.is_ok());
If you’re interested in building asynchronous HTTP clients in Rust, tokio-curl might be worth considering. This library provides a futures-based interface to the libcurl HTTP library, which is a widely-used and well-respected C library for sending HTTP requests.
By building on top of libcurl in Rust, tokio-curl allows you to leverage the power and stability of this library while taking advantage of the asynchronous programming model provided by Rust’s tokio library.
Whether you need to send simple HTTP requests or more complex ones with custom headers and authentication, tokio-curl provides a range of features and options to help you get the job done. If you’re looking for a reliable and efficient way to build asynchronous HTTP clients in Rust, tokio-curl is definitely worth exploring. See the library in action below:
extern crate curl; extern crate futures; extern crate tokio_core; extern crate tokio_curl; use std::io::{self, Write}; use curl::easy::Easy; use futures::Future; use tokio_core::reactor::Core; use tokio_curl::Session; fn main() { // Create an event loop that we'll run on, as well as an HTTP `Session` // which we'll be routing all requests through. let mut lp = Core::new().unwrap(); let session = Session::new(lp.handle()); // Prepare the HTTP request to be sent. let mut req = Easy::new(); req.get(true).unwrap(); req.url("https://www.rust-lang.org").unwrap(); req.write_function(|data| { io::stdout().write_all(data).unwrap(); Ok(data.len()) }).unwrap(); // Once we've got our session, issue an HTTP request to download the // rust-lang home page let request = session.perform(req); // Execute the request, and print the response code as well as the error // that happened (if any). let mut req = lp.run(request).unwrap(); println!("{:?}", req.response_code()); }
If you want a low-level HTTP library in Rust, I recommend using hyper. It’s production-ready and fully written in Rust so you don’t have to worry too much about safety issues. Moreover, it’s the only library that mentions production readiness.
For a more high-level HTTP library, I would choose reqwest. The library is built on top of hyper, so it offers many of the same advantages as hyper and has a more convenient syntax to boot.
Throughout this guide, we touched upon other libraries that can be useful in a range of other situations. If you have to work with curl, you should explore curl-rust, Isahc, or even Surf. If you’re looking for a lightweight library that doesn’t add much to compile time, ureq is the way to go. However, note that ureq only supports synchronous requests.
Hopefully, this comparison helps you select the best HTTP client for your next Rust project.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.
2 Replies to "How to choose the right Rust HTTP client"
Thanks for the article! đź‘Ť
Note: Second item contains a duplicated example from the first one.
You are right! Something went wrong there, we’ll fix it soon! Thanks for notifiying 🙂