Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HTTP client in Rust

There are a number of Crates that can help us build HTTP requests. The most popular seems to be reqwest. We are going to take a look at it.

Herbert Wolverson commented that reqwest spawns a tokio thread even when working in blocking mode which is quite expensieve and hes suggested ureq. We'll take a look at that as well.

We'll use httpbin for checking examples.

httpbin

httpbin is a service to test HTTP client implementations. We are going to use it extensively to see examples of code.

The service is quite reliable, but it isn't always available, but luckily the developer provides it as a Docker container as well.

So if you have Docker installed on your computer you can run the service locally.

Run httpbin locally

Start as:

docker run --rm -p 80:80 --name httpbin kennethreitz/httpbin

Then run the code:

cargo run localhost

To stop the Docker container open another terminal and execute

docker container stop -t0 httpbin

Reqwest the HTTP client library of Rust

Using the reqwest crate.

Reqwest is a Rust crate to handle HTTP requests.

In order to handle secure HTTPS request I had to install the following packages on Ubuntu:

apt-get install pkg-config
apt-get install libssl-dev

Simple blocking http client with reqwest sending a GET request

Create a new crate

cargo new demo
cd demo

Add the reqwest crate as a dependency with the blocking feature:

cargo add reqwest --features blocking

Cargo.toml

Or manually edit the Cargo.toml file to add it as a dependency.

[package]
name = "simple-http-client"
version = "0.1.0"
edition = "2024"

[dependencies]
reqwest = { version = "0.12.23", features = ["blocking"] }

Code

fn main() {
    let url = get_url("get");

    let res = match reqwest::blocking::get(url) {
        Ok(res) => res,
        Err(err) => {
            println!("Error {}", err);
            std::process::exit(1);
        }
    };
    println!("{:?}", res.status());
    println!("{:?}", res);
}

fn get_url(path: &str) -> String {
    let host = std::env::args().nth(1).unwrap_or("httpbin.org".into());
    let url = if host == "localhost" {
        format!("http://localhost/{path}")
    } else {
        format!("https://{host}/{path}")
    };

    url
}

Run the code

cargo run

If everything works fine then you'll get back something like this:

200
Response {
   url: "https://httpbin.org/get",
   status: 200,
   headers: {
     "date": "Wed, 24 Sep 2025 18:09:11 GMT",
     "content-type": "application/json",
     "content-length": "219",
     "connection": "keep-alive",
     "server": "gunicorn/19.9.0",
     "access-control-allow-origin": "*",
     "access-control-allow-credentials": "true"
  }
}

If the service does not work we'll see an error message:

Error error sending request for url (https://httpbin.org/get)

or you might get this output:

503
Response {
    url: "https://httpbin.org/get",
    status: 503,
    headers: {
        "server": "awselb/2.0",
        "date": "Wed, 24 Sep 2025 18:30:49 GMT",
        "content-type": "text/html",
        "content-length": "162",
        "connection": "keep-alive"
    }
}

Run httpbin locally

In that case we can also run the httpbin service locally using Docker.

Start as:

docker run --rm -p 80:80 --name httpbin kennethreitz/httpbin

Then run the code:

cargo run localhost

To stop the Docker container open another terminal and execute

docker container stop -t0 httpbin

  • reqwest
  • blocking
  • get
  • status

Simple blocking HTTP GET request in Rust

The reqwest crate provides functions for both asynchronous and blocking http requests. Although in most cases you'd probably want to use the asynch calls, using the blocking calls is simpler, so we start with that.

This is what we add to the Cargo.toml

[package]
name = "simple-blocking-http-get-request"
version = "0.1.0"
edition = "2024"

[dependencies]
reqwest = { version = "0.11.20", features = ["blocking"] }

And this is the code:


fn main() {
    let res = match reqwest::blocking::get("https://httpbin.org/ip") {
        Ok(res) => res,
        Err(err) => {
            println!("Error {}", err);
            std::process::exit(1);
        }
    };

    println!("{:?}", res);

    println!("status: {:?}", res.status());

    println!("server: {:?}", &res.headers()["server"]);

    match res.text() {
        Ok(val) => println!("{}", val),
        Err(err) => eprintln!("Error: {}", err),
    };

}

This is the output (slightly reformatted to make it easier to read).

Response {
    url: Url {
        scheme: "https",
        cannot_be_a_base: false,
        username: "",
        password: None,
        host: Some(Domain("httpbin.org")),
        port: None,
        path: "/ip",
        query: None,
        fragment: None
    },
    status: 200,
    headers: {
        "date": "Tue, 03 Oct 2023 13:12:58 GMT",
        "content-type": "application/json",
        "content-length": "31",
        "connection": "keep-alive",
        "server": "gunicorn/19.9.0",
        "access-control-allow-origin": "*",
        "access-control-allow-credentials": "true"
    }
}

status: 200

server: "gunicorn/19.9.0"

{
  "origin": "46.120.9.250"
}

  • reqwest
  • blocking
  • HTTP
  • GET

Simple blocking HTTP POST request using Rust

The reqwest crate provides all the capabilities to send HTTP requests.

In this example we are using the reqwest crate to send an HTTP POST request to https://httpbin.org/.

The curl command

This is the same command as executed using curl.

curl -X POST -d "text=Hello World!" http://httpbin.org/post

This is the result:

{
  "args": {},
  "data": "",
  "files": {},
  "form": {
    "text": "Hello World!"
  },
  "headers": {
    "Accept": "*/*",
    "Content-Length": "17",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.2.1",
    "X-Amzn-Trace-Id": "Root=1-65b22590-3ba351816c46408426023f1b"
  },
  "json": null,
  "origin": "46.120.9.250",
  "url": "http://httpbin.org/post"
}

Dependencies

In order to be able to send a blocking request we need to add that feature, and in order to be able to parse the returned JSON data we had to add the json feature.

[package]
name = "simple-blocking-http-post-reqwest"
version = "0.1.0"
edition = "2024"

[dependencies]
reqwest = { version = "0.11.23", features = ["blocking", "json"] }
serde_json = "1.0.111"

The code

We can pass the parameters in the form method as a bunch of key-value pairs from a HashMap.

use reqwest::blocking::Client;
use std::collections::HashMap;

fn main() {
    let client = Client::new();
    let params = HashMap::from([
        ("text", "Hello World!"),
    ]);

    match client.post("http://httpbin.org/post").form(&params).send() {
        Ok(resp) => {
            let data:  serde_json::Value = resp.json().unwrap();
            println!("{:#?}", data);
        },
        Err(err) => eprintln!("{}", err),
    }
}

The result

This is the resulting data from the request.

#![allow(unused)]
fn main() {
Object {
    "args": Object {},
    "data": String(""),
    "files": Object {},
    "form": Object {
        "text": String("Hello World!"),
    },
    "headers": Object {
        "Accept": String("*/*"),
        "Content-Length": String("19"),
        "Content-Type": String("application/x-www-form-urlencoded"),
        "Host": String("httpbin.org"),
        "X-Amzn-Trace-Id": String("Root=1-65b225c3-1cfb5c6440c0d3014b818197"),
    },
    "json": Null,
    "origin": String("46.120.9.250"),
    "url": String("http://httpbin.org/post"),
}
}

  • reqwest
  • HTTP
  • POST
  • form

Set the User-Agent in a HTTP request using Rust reqwest

Dependencies

[package]
name = "reqwest-set-user-agent"
version = "0.1.0"
edition = "2024"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = { version = "0.11", features = ["blocking"] }

The code


use reqwest::header::USER_AGENT;

fn main() {
    let client = reqwest::blocking::Client::new();

    let res = client
    .get("http://httpbin.org/headers")
    .send().unwrap();
    println!("{}", res.text().unwrap());

    let res = client
    .get("http://httpbin.org/headers")
    .header(USER_AGENT, "Rust Maven 1.42")
    .send().unwrap();
    println!("{}", res.text().unwrap());
}

The output

{
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "X-Amzn-Trace-Id": "Root=1-65b23d5c-7291d26c5121b8d160a837f9"
  }
}

{
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "Rust Maven 1.42",
    "X-Amzn-Trace-Id": "Root=1-65b23d5c-1376c1c654589f201d8f958c"
  }
}

  • User Agent
  • reqwest
  • header

HTTP reqwest sending cookie

Sending a cookie back to the server using the reqwest crate.

The curl command

curl --cookie counter=42 "https://httpbin.org/get"

and the output:

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Cookie": "counter=42",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.2.1",
    "X-Amzn-Trace-Id": "Root=1-65b2455d-400413860278b6624dc30284"
  },
  "origin": "46.120.9.250",
  "url": "https://httpbin.org/get"
}

The dependencies

[package]
name = "simple-blocking-http-reqwest-sending-cookie"
version = "0.1.0"
edition = "2024"

[dependencies]
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde_json = "1.0"

The code

use reqwest::blocking::Client;

fn main() {
    let client = Client::new();

    match client.get("https://httpbin.org/get").header("Cookie", "counter=42").send() {
        Ok(resp) => {
            let data:  serde_json::Value = resp.json().unwrap();
            println!("{:#?}", data);
        },
        Err(err) => eprintln!("{}", err),
    }
}

The output

Object {
    "args": Object {},
    "headers": Object {
        "Accept": String("*/*"),
        "Cookie": String("counter=42"),
        "Host": String("httpbin.org"),
        "X-Amzn-Trace-Id": String("Root=1-65b244fe-37f44f2f5385618e52e9397b"),
    },
    "origin": String("46.120.9.250"),
    "url": String("https://httpbin.org/get"),
}

  • reqwest
  • header
  • Cookie
  • Client

http-client async with reqwest

In order to send asynchronous requests we need to depend on both the reqwest crate and on tokio that provides the asynchronous runtime. For the latter we add the full feature so we won't need to worry about anything missing.

cargo add reqwest
cargo add tokio -F full

Cargo.toml

[package]
name = "http-client"
version = "0.1.0"
edition = "2024"

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

The Code

In this example we are not particularily interested in any specific errors, so we handle them using the ? operator that, in this case, will make our program exit with a non-zero exit code.

The text method will return the content of the web page. As we are accessing the main page of httpbin.org that should be some HTML document.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = get_url("");

    let html = reqwest::get(url).await?.text().await?;
    println!("{html}");
    Ok(())
}

fn get_url(path: &str) -> String {
    let host = std::env::args().nth(1).unwrap_or("httpbin.org".into());
    let url = if host == "localhost" {
        format!("http://localhost/{path}")
    } else {
        format!("https://{host}/{path}")
    };

    url
}

  • reqwest
  • async
  • tokio

Async reqwest - get IP in a simple JSON structure

In this example we are going to retrieve some data in JSON format. To make this process easy first we are going to retrieve a JSON structure that only has a single key-value pair. The IP address of the client.

httpbin.org provides many API endpoints, one of them is the /ip endpoint. Sending a GET HTTP request to this endpoint we get back a JSON structure that looks like this:

{
  "origin": "47.131.10.23"
}

Dependencies

In order for this to work we need to add the json feature to the reqwest crate. We still use tokio with the full feature as earlier.

cargo add reqwest -F json
cargo add tokio -F full

Cargo.toml

[package]
name = "http-client"
version = "0.1.0"
edition = "2024"

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

The Code

use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = get_url("ip");

    let raw = reqwest::get(&url).await?.text().await?;
    println!("{raw}");

    let resp = reqwest::get(url)
        .await?
        .json::<HashMap<String, String>>()
        .await?;
    println!("{:#?}", resp);
    println!("{}", resp["origin"]);
    Ok(())
}


fn get_url(path: &str) -> String {
    let host = std::env::args().nth(1).unwrap_or("httpbin.org".into());
    let url = if host == "localhost" {
        format!("http://localhost/{path}")
    } else {
        format!("https://{host}/{path}")
    };

    url
}

We have the helper function get_url that allows us to use either the public web site or the one we run locally using Docker.

The first 2 lines are effectively the same as in the previous example, we just retrieve the response as plain text or raw text, if you wish. You can do this to observer the structure of the JSON data. We don't need this for the real code.

#![allow(unused)]
fn main() {
let raw = reqwest::get(&url).await?.text().await?;
println!("{raw}");
}

Then comes the real code that instead of fetching the content as text it fetches it as json and immediately converts it to a HashMap where both keys and values are Strings.

The debugging printing of Rust will look quite similar to the original structure except of the trailing comma, and the indentation, I guess.

We can then access the field called origin.

That was rather painless because the JSON was very simple.

The output

cargo run
{
  "origin": "47.131.10.23"
}

{
    "origin": "47.131.10.23",
}
47.131.10.23

  • json
  • HashMap

Async reqwest - set User Agent

cargo add reqwest -F json
cargo add tokio -F full

Cargo.toml

[package]
name = "async-reqwest-set-user-agent"
version = "0.1.0"
edition = "2024"

[dependencies]
reqwest = { version = "0.12.23", features = ["json"] }
tokio = { version = "1.47.1", features = ["full"] }

The code

use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = get_url("headers");

    let resp = reqwest::get(url)
        .await?
        .json::<HashMap<String, String>>()
        .await?;
    println!("{:#?}", resp);
    Ok(())
}

fn get_url(path: &str) -> String {
    let host = std::env::args().nth(1).unwrap_or("httpbin.org".into());
    let url = if host == "localhost" {
        format!("http://localhost/{path}")
    } else {
        format!("https://{host}/{path}")
    };

    url
}

Download many URLs in parallel (async)

use regex::Regex;
use std::sync::Arc;
use tokio::join;
use tokio::task::JoinHandle;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();

    let url = "https://rust.code-maven.com/sitemap.xml";
    let resp = reqwest::get(url).await?;
    //println!("{:#?}", resp);
    //println!("{:#?}", resp.status());
    let text = resp.text().await?;
    //println!("{}", text);
    // <loc>https://rust.code-maven.com/</loc>

    let re = Regex::new(r"<loc>([^<]+)</loc>").unwrap();

    let mut tasks: Vec<JoinHandle<Result<(), ()>>> = vec![];

    let mut count = 0;
    for capture in re.captures_iter(&text) {
        //println!("Full match: '{}'", &capture[0]);
        //println!("URL match: '{}'", &capture[1]);
        let path = Arc::new(capture[1].to_owned());
        {
            let path = path.clone();
            tasks.push(tokio::spawn(async move {
                match reqwest::get(&*path).await {
                    Ok(resp) => match resp.text().await {
                        Ok(text) => {
                            println!("RESPONSE: {} bytes from {}", text.len(), path);
                        }
                        Err(_) => println!("ERROR reading {}", path),
                    },
                    Err(_) => println!("ERROR downloading {}", path),
                }
                Ok(())
            }));
        }
        println!("{path}");

        count += 1;
        if count > 10 {
            break;
        }
    }

    println!("Started {} tasks. Waiting...", tasks.len());
    for task in tasks {
        let _r = join!(task);
    }
    //join_all(tasks).await;

    println!("Elapsed time: {:?}", start.elapsed());
    Ok(())
}
[package]
name = "download-rust-maven"
version = "0.1.0"
edition = "2024"

[dependencies]
regex = "1.10.5"
reqwest = "0.12.5"
tokio = { version = "1.38.1", features = ["full"] }

Based on this article


  • reqwest
  • async
  • tokio

Blocking HTTP GET request with ureq

ureq was recommended as a better alternative to reqwest for blocking requests.

In this example we'll see how to use it to send a simple GET request.

Dependencies

[package]
name = "get"
version = "0.1.0"
edition = "2024"

[dependencies]
ureq = "3.1.2"

Code

get_url just gets the url from the command line

We get a response that can be either a good response or an error. In case of a good response we can print the content of the response.

Success

$ cargo run http://localhost/get

GET request to: http://localhost/get
Status: 200 OK
------- Headers --------
server: "gunicorn/19.9.0"
date: "Tue, 14 Oct 2025 10:07:54 GMT"
connection: "keep-alive"
content-type: "application/json"
content-length: "210"
access-control-allow-origin: "*"
access-control-allow-credentials: "true"
------- Body --------
Body: {
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip", 
    "Host": "localhost", 
    "User-Agent": "ureq/3.1.2"
  }, 
  "origin": "172.17.0.1", 
  "url": "http://localhost/get"
}

404 status code

$ cargo run -q http://localhost/status/404

GET request to: http://localhost/status/404
Error: http status: 404

500 status code

$ cargo run -q http://localhost/status/500

GET request to: http://localhost/status/500
Error: http status: 500

Redirect:

This will follow the redirect and thus it will print out the content of the target web site.

$ cargo run "http://localhost/redirect-to?url=https://rust.code-maven.com/&status_code=301"
fn main() {
    let url = get_url();
    println!("GET request to: {}", url);
    let response = ureq::get(&url).call();
    match response {
        Ok(mut resp) => {
            println!("Status: {}", resp.status());

            println!("------- Headers --------");
            resp.headers_mut().iter().for_each(|(k, v)| {
                println!("{}: {:?}", k, v);
            });

            println!("------- Body --------");
            let body = resp.body_mut();
            if let Ok(content) = body.read_to_string() {
                println!("Body: {content}");
            } else {
                eprintln!("Failed to read response body");
                return;
            };
        }
        Err(err) => {
            eprintln!("Error: {}", err);
        }
    }
}

fn get_url() -> String {
    let args = std::env::args().collect::<Vec<String>>();
    if args.len() < 2 {
        eprintln!("Usage: {} <URL>", args[0]);
        std::process::exit(1);
    }
    args[1].clone()
}

DRAFT: Accept cookies in an HTTP request sent by the server

This is a DARFT!

curl -i "http://httpbin.org/cookies/set?name=Foo+Bar"

In this request we asked the httpbin server to send us a cookie (normally this is the decision of the server, but the httpbin server is here to help us).

Output (showing only the header part) where we can see the row Set-Cookie, that is setting a cookie in our "broswer".

HTTP/1.1 302 FOUND
Date: Thu, 25 Jan 2024 13:22:52 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 223
Connection: keep-alive
Server: gunicorn/19.9.0
Location: /cookies
Set-Cookie: name="Foo Bar"; Path=/
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Dependencies

[package]
name = "reqwest-accept-cookies"
version = "0.1.0"
edition = "2024"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = { version = "0.11", features = ["blocking"] }

The code

use reqwest::header::USER_AGENT;

fn main() {
    let custom = reqwest::redirect::Policy::custom(|attempt| { attempt.stop() });

    //let client = reqwest::blocking::Client::new();
    let client = reqwest::blocking::Client::builder()
    .redirect(custom)
    .build().unwrap();

    let res = client
    .get("http://httpbin.org/cookies/set?name=Foo")
    .header(USER_AGENT, "Rust Maven 1.42")
    .send().unwrap();
    //println!("{}", res.text().unwrap());
    //let c = res.headers().get("Date").unwrap();
    println!("{}", res.headers().get("Date").unwrap().to_str().unwrap());
    println!("{}", res.headers().get("set-cookie").unwrap().to_str().unwrap());
    for (name, value) in res.headers() {
        println!("{}", name.as_str());
    }
}

The output