As part of my Rust journey, I’m building a simple CLI to get rid of manual tasks I’ve to complete on regular basis. Some of those tasks can be automated by leveraging a HTTP client in Rust. A quick research led me to reqwest (https://github.com/seanmonstar/reqwest), a simple HTTP client for Rust that fits my needs and let me get the job done quickly.

You may ask yourself if reqwest is the best HTTP client for Rust? To be honest, I don’t know (yet). But I got my job done quickly, and I really enjoyed the API surface offered by reqwest. That said let’s dive into reqwest and see how it actually looks like.

Install reqwest

You can install reqwest by simply adding it to your Cargo.toml along with the famous Tokio crate, which is the asynchronous runtime used under the hood of reqwest.

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }

Although reqwest can work with different Content-Types, this post focuses on dealing with JSON because most of the HTTP APIs I deal with expose data in this particular format.

A simple GET request with reqwest

Let’s issue a simple GET request to see how HTTP calls with reqwest actually look like.

let b = reqwest::get("https://swapi.dev/api/people")
    .await?
    .json()
    .await?;

println!("Got {:?}", b);

This sample uses the convenient method get to quickly issue a simple HTTP GET request. Chances are good, that you issue many different requests as part of your application. If that’s the case, you should consider creating a dedicated Client and reusing it for multiple, independent HTTP requests.

Client and RequestBuilder in reqwest

By using a dedicated Client, you can quickly create new requests of different kinds. Client offers methods like get, post, put, delete, …, and a more general request(&self, method: Method, url: U) method which you can use to create a new RequestBuilder. You can use aRequestBuilder to customize all aspects of a certain request before issuing it. Functionality provided by the RequestBuilder is designed as a chained (or fluent) API. For example consider the following GET request, which adds additional HTTP headers and specifies a custom timeout, before issuing the request:

use reqwest;
use std::error::Error;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let client = reqwest::Client::new();
    let doge = client
        .get("https://api.coinstats.app/public/v1/coins/dogecoin")
        .header("Accept", "text/plain")
        .timeout(Duration::from_secs(3))
        .send()
        .await?
        .text()
        .await?;
    println!("{:}", doge);
    Ok(())
}

Default HTTP headers

In the previous sample, the Accept HTTP header was specified using the header function on RequestBuilder. You can also use the ClientBuilder to specify HTTP headers for all requests issued:

use reqwest;
use reqwest::header;
use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let mut h = header::HeaderMap::new();
    h.insert("Accept", header::HeaderValue::from_static("application/json"));
    
    let client = reqwest::Client::builder()
        .default_headers(h)
        .build()?;

    let doge = client
        .get("https://api.coinstats.app/public/v1/coins/dogecoin")
        .send()
        .await?
        .text()
        .await?;
    println!("{:}", doge);
    Ok(())
}

Decompression GZIP responses with reqwest

When facing an API that exposes compressed data using GZIP, you can decompress responses on the fly. This requires the gzip feature being enabled for the reqwest crate (configured in Cargo.toml). Automatic GZIP decompression is enabled by calling the gzip function on the ClientBuilder:

// omitted
let client = reqwest::Client::builder()
    .gzip(true)
    .default_headers(h)
    .build()?;
// omitted

Response bodies will be decompressed automatically, if the server sends Content-Encoding: gzip HTTP header as part of the HTTP response.

Deserialize JSON response body

To deserialize a response into a struct, add serde to the project in your Cargo.toml.

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }

Obviously, you’ve to recreate the response structure in Rust, and use Response.json(). You can either provide a type for the desired variable explicitly (let p: Response = r::json().await?;) , or you call json() using the turbo-fish syntax (let p = r.json::<Response>().await?;) as shown here:

use std::error::Error;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Response {
    coins: Vec<Coin>,
}

#[derive(Deserialize, Debug)]
struct Coin {
    id: String,
    name: String,
    icon: String,
    symbol: String,
    price: f32,
    priceBtc: f32,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let http_response = reqwest::get("https://api.coinstats.app/public/v1/coins?skip=0&limit=10").await?;
    let response = http_response.json::<Response>().await?;
    println!("{:#?}", response.coins);
    Ok(())
}

Custom HTTP Proxies with reqwest

reqwest works with HTTP proxies by default. HTTP proxy URLs are automatically loaded from the HTTP_PROXY and HTTPS_PROXY environment variables if set. You can also overwrite those settings by using the Proxy struct as shown here:

// omitted
let client = reqwest::Client::builder()
    .proxy(reqwest::Proxy::https("https://my-proxy.local")
    .build()?;
// omitted

You can also disable a system-wide enabled proxy by calling no_proxy() on ClientBuilder:

// omitted
let client = reqwest::Client::builder()
    .no_proxy()
    .build()?;
// omitted

Conclusion

Using reqwest, I interacted with HTTP APIs as part of my research comfortably. It’s just another HTTP client library, nothing fancy here 🙃. It’s solid; it works for my requirements. I like the fluent API design, which allows me to call into given HTTP(s) APIs quickly. That said, it is worth taking a look at reqwest if you haven’t yet.