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.
-
We use httpbin for checking examples.
-
Simple blocking HTTP POST request using Rust -
POST,Client. -
Set the User-Agent in a HTTP request using Rust reqwest -
header. -
HTTP reqwest sending cookie -
header,Cookie.
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(¶ms).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()); } }