Rust - Reqwest examples
Tags: rust, webdev, tutorial, reqwest
Published: 2021-03-11 11:20:57

### Introduction

There are 3 things that need to happen:
* Building a client that can be reused across multiple requests
* Performing the execution of the network request
* Parsing the response

```rust
use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build the client using the builder pattern
    let client = reqwest::Client::builder()
        .build()?;
    
    // Perform the actual execution of the network request
    let res = client
        .get("https://httpbin.org/ip")
        .send()
        .await?;
        
    // Parse the response body as Json in this case
    let ip = res
        .json::<HashMap<String, String>>()
        .await?;
    
    println!("{:?}", ip);
    Ok(())
}
```

Instead of parsing the json body as a dynamic hash-map, you could use a well-defined struct

```rust
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Ip {
    origin: String
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build the client using the builder pattern
    let client = reqwest::Client::builder()
        .build()?;
    
    // Perform the actual execution of the network request
    let res = client
        .get("https://httpbin.org/ip")
        .send()
        .await?;
        
    // Parse the response body as Json in this case
    let ip = res
        .json::<Ip>()
        .await?;
    
    println!("{:?}", ip);
    Ok(())
}
```
### Query Strings
```rust
let client = reqwest::Client::builder()
    .build()?;

let res = client
    .get("http://httpbin.org")
    .query(&[("foo", "bar")])
    .send()?;
```

### POST JSON Data
The `json(..)` method on the `RequestBuilder` takes any value that can be serialized into JSON such as a HashMap or a Struct.
```rust
let mut map = HashMap::new();
map.insert("foo", "bar");
map.insert("buzz", "blah");

...
.json(&map)
```

```rust
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Serialize, Deserialize)]
struct Person {
    first_name: String,
    last_name: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct PersonResponse {
    data: String,
    method: String,
    headers: HashMap<String, String>
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let p = Person { 
        first_name: "Foo".into(),
        last_name: "Bar".into(),
    };

    let res = reqwest::Client::new()
        .post("https://httpbin.org/anything")
        .json(&p)
        .send()
        .await?;

    let js = res
        .json::<PersonResponse>()
        .await?;

    let person: Person = serde_json::from_str(&js.data)?;
    println!("{:#?}", person);
    
    println!("Headers: {:#?}", js.headers);
    Ok(())
}

```

### POST Form data
```rust
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Person {
    first_name: String,
    last_name: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let p = Person { 
        first_name: "Foo".into(),
        last_name: "Bar".into(),
    };

    let res = reqwest::Client::new()
        .post("https://httpbin.org/anything")
        .form(&p)
        .send()
        .await?;

    let t = res
        .text()
        .await?;

    println!("{}", t);
    
// 	$ cargo run
// 	    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
// 	     Running `target/debug/reqwest-tut`
// 	{
// 	  "args": {}, 
// 	  "data": "", 
// 	  "files": {}, 
// 	  "form": {
// 	    "first_name": "Foo", 
// 	    "last_name": "Bar"
// 	  }, 
// 	  "headers": {
// 	    "Accept": "*/*", 
// 	    "Content-Length": "28", 
// 	    "Content-Type": "application/x-www-form-urlencoded", 
// 	    "Host": "httpbin.org", 
// 	    "X-Amzn-Trace-Id": "Root=1-60453174-384d97870199933007fbb388"
// 	  }, 
// 	  "json": null, 
// 	  "method": "POST", 
// 	  "origin": "125.125.104.53", 
// 	  "url": "https://httpbin.org/anything"
// 	}
// 	
// 	
    Ok(())
}

```

### Request Headers
You can set the headers by calling the `header(.., ..)` method on the request
```rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::builder()
        .build()?;
    
    let res = client
        .post("https://httpbin.org/anything")
        .body("arbitrary text")
        .header("X-Person-First", "Foo!")
        .header("X-Person-Last", "Bar!!")
        .send()
        .await?;

    let t = res
        .text()
        .await?;

    println!("{}", t);
// {
//   "args": {}, 
//   "data": "arbitrary text", 
//   "files": {}, 
//   "form": {}, 
//   "headers": {
//     "Accept": "*/*", 
//     "Content-Length": "14", 
//     "Host": "httpbin.org", 
//     "X-Amzn-Trace-Id": "Root=1-604538df-218a5bb97264e7130c298b23", 
//     "X-Person-First": "Foo!", 
//     "X-Person-Last": "Bar!!"
//   }, 
//   "json": null, 
//   "method": "POST", 
//   "origin": "49.206.4.160", 
//   "url": "https://httpbin.org/anything"
// }

    Ok(())
}

```

You can also set default headers on the client that can be overriden while making individual requests
```rust
use reqwest::header;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let mut headers = header::HeaderMap::new();
    headers.insert("X-HEADER-1", header::HeaderValue::from_static("val1"));
    headers.insert("X-HEADER-2", header::HeaderValue::from_static("val2"));

    let client = reqwest::Client::builder()
        .default_headers(headers)
        .build()?;

    let res = client
        .get("https://httpbin.org/anything")
        .body("whatever")
        .header("X-HEADER-1", "overriden val1")
        .send()
        .await?;

    println!("{}", res.text().await?);

    // {
    //   "args": {},
    //   "data": "whatever",
    //   "files": {},
    //   "form": {},
    //   "headers": {
    //     "Accept": "*/*",
    //     "Content-Length": "8",
    //     "Host": "httpbin.org",
    //     "X-Amzn-Trace-Id": "Root=1-60453dad-429e59a434bd460b0a48e7d5",
    //     "X-Header-1": "overriden val1",
    //     "X-Header-2": "val2"
    //   },
    //   "json": null,
    //   "method": "GET",
    //   "origin": "49.206.4.160",
    //   "url": "https://httpbin.org/anything"
    // }

    Ok(())
}
```
### Status Code
```rust
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let client = reqwest::Client::builder().build()?;

    let res = client
        .get("https://httpbin.org/status/400")
        .send()
        .await?;

    match res.status() {
        reqwest::StatusCode::BAD_REQUEST => println!(
            "content-length:{:?} server:{:?}", 
            res.headers().get(reqwest::header::CONTENT_LENGTH),
            res.headers().get(reqwest::header::SERVER),
        ),
        status => println!("status: {}", status),
    }

    // content-length:Some("0") server:Some("gunicorn/19.9.0")
    Ok(())
}
```

### Timeouts
#### Client Timeout
```rust
let client = reqwest::Client::builder()
    .timeout(std::time::Duration::from_millis(500))
    .build()?;

```
#### Connect Timeout
```rust
let client = reqwest::Client::builder()
    .connect_timeout(std::time::Duration::from_millis(100))
    .build()?;
```
#### Request Timeout
```rust
let res = client
    .get(&url)
    .timeout(std::time::Duration::from_millis(500))
    .send()
    .await;
```

### Download data
```rust
use std::fs::File;
use std::io;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::builder().build()?;

    let res = client
        .get("https://httpbin.org/image/png")
        .send()
        .await?
        .bytes()
        .await?;

    let mut data = res.as_ref();

    let mut f = File::create("i.png")?;

    io::copy(&mut data, &mut f)?;

    Ok(())
}

// $ file i.png 
// i.png: PNG image data, 100 x 100, 8-bit/color RGB, non-interlaced 
```

##### Dependency configuration for the samples
```
[dependencies]
reqwest = { version = "0.11", features = ["json", "blocking", "cookies"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0" 
futures = "0.3"
```