Calling a Web API

Query the GitHub API

reqwest serde cat-network-programming cat-encoding cat-web-programming cat-web-programming::http-client

Queries GitHub stargazers API v3⮳ with reqwest::get⮳ to get list of all users who have marked a GitHub project with a star. reqwest::Response⮳ is deserialized with reqwest::Response::json⮳ into User objects implementing serde::Deserialize⮳.

tokio::main is used to set up the async executor and the process waits for reqwest::get to complete before processing the response into User instances.

use reqwest::Error;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct User {
    login: String,
    id: u32,
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    let request_url = format!(
        "https://api.github.com/repos/{owner}/{repo}/stargazers",
        owner = "john-cd",
        repo = "rust_howto"
    );
    println!("{}", request_url);

    let client = reqwest::Client::builder().user_agent("Rust-test").build()?;

    let response = client.get(&request_url).send().await?;

    let users: Vec<User> = response.json().await?;
    println!("{:?}", users);
    Ok(())
}

Check if an API resource exists

reqwest cat-network-programming cat-web-programming cat-web-programming::http-client

Query the GitHub Users Endpoint using a HEAD request reqwest::Client::head⮳ and then inspect the response code to determine success. This is a quick way to query a rest resource without needing to receive a body. reqwest::Client⮳ configured with reqwest::ClientBuilder::timeout⮳ ensures a request will not last longer than a timeout.

Due to both reqwest::ClientBuilder::build⮳ and reqwest::RequestBuilder::send⮳ returning reqwest::Error⮳ types, the shortcut reqwest::Result⮳ is used for the main function return type.

use std::time::Duration;

use reqwest::ClientBuilder;

#[tokio::main]
async fn main() -> reqwest::Result<()> {
    let user = "ferris-the-crab";
    let request_url = format!("https://api.github.com/users/{}", user);
    println!("{}", request_url);

    let timeout = Duration::new(5, 0);
    let client = ClientBuilder::new().timeout(timeout).build()?;
    let response = client.head(&request_url).send().await?;

    if response.status().is_success() {
        println!("{} is a user!", user);
    } else {
        println!("{} is not a user!", user);
    }

    Ok(())
}

Create and delete Gist with GitHub API

reqwest serde cat-network-programming cat-encoding cat-web-programming cat-web-programming::http-client

Creates a gist with POST request to GitHub gists API v3⮳ using reqwest::Client::post⮳ and removes it with DELETE request using reqwest::Client::post⮳.

The reqwest::Client⮳ is responsible for details of both requests including URL, body and authentication. The POST body from reqwest::Client⮳ macro provides arbitrary JSON body. Call to reqwest::Client⮳ sets the request body. reqwest::Client⮳ handles authentication. The call to reqwest::Client⮳ synchronously executes the requests.

use std::collections::HashMap;
use std::env;

use anyhow::Result;
use reqwest::Client;
use serde::Deserialize;
use serde::Serialize;

#[derive(Deserialize, Serialize, Debug)]
struct Post<'a> {
    description: &'a str,
    public: bool,
    files: HashMap<&'a str, Content<'a>>,
}

#[derive(Deserialize, Serialize, Debug)]
struct Content<'a> {
    content: &'a str,
}

#[derive(Deserialize, Debug)]
struct Gist {
    id: String,
    html_url: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    let gh_user = env::var("GH_USER")?;
    let gh_pass = env::var("GH_PASS")?;

    // Example POST to the GitHub gists API
    let gist_body = Post {
        description: "the description for this gist",
        public: true,
        files: {
            let mut h = HashMap::new();
            h.insert(
                "main.rs",
                Content {
                    content: r#"
fn main() { println!("hello world!");}
"#,
                },
            );
            h
        },
    };

    let request_url = "https://api.github.com/gists";
    let response = Client::new()
        .post(request_url)
        .basic_auth(gh_user.clone(), Some(gh_pass.clone()))
        .json(&gist_body)
        .send()
        .await?;

    let gist: Gist = response.json().await?;
    println!("Created {:?}", gist);

    let request_url = format!("{}/{}", request_url, gist.id);
    let response = Client::new()
        .delete(&request_url)
        .basic_auth(gh_user, Some(gh_pass))
        .send()
        .await?;

    println!(
        "Gist {} deleted! Status code: {}",
        gist.id,
        response.status()
    );
    Ok(())
}

The example uses HTTP basic auth⮳ in order to authorize access to GitHub API⮳. Typical use case would employ one of the much more complex OAuth⮳ authorization flows.

Consume a paginated RESTful API

reqwest serde cat-network-programming cat-encoding cat-web-programming cat-web-programming::http-client

Wraps a paginated web API in a convenient Rust iterator. The iterator lazily fetches the next page of results from the remote server as it arrives at the end of each page.

// TODO fix - the API no longer returns a crate_id - need to get
// version_id then join version_id and Version.id ro retrieve crate
// name Consider a simpler API for example purposes

// use anyhow::Result;
// use serde::Deserialize;

// // Structs used to deserialize the JSON produced by the API

// #[derive(Deserialize)]
// struct ApiResponse {
//     dependencies: Vec<Dependency>,
//     meta: Meta,
//     versions: Vec<Version>,
// }

// // https://github.com/rust-lang/crates.io/issues/856
// #[derive(Deserialize)]
// struct Dependency {
//     version_id: String,
// }

// #[derive(Deserialize)]
// struct Meta {
//     total: u32,
// }

// #[derive(Deserialize)]
// struct Version {
//     id: String,
//     #[serde(alias = "crate")]
//     crate_id: String,
// }

// // Main struct

// struct ReverseDependencies {
//     crate_id: String,
//     dependencies: <Vec<Dependency> as IntoIterator>::IntoIter,
//     client: reqwest::blocking::Client,
//     page: u32,
//     per_page: u32,
//     total: u32,
// }

// impl ReverseDependencies {
//     fn of(crate_id: &str) -> Result<Self> {
//         let client = reqwest::blocking::Client::builder()
//             .user_agent("Rust-test")
//             .build()?;
//         Ok(ReverseDependencies {
//             crate_id: crate_id.to_owned(),
//             dependencies: vec![].into_iter(),
//             client,
//             page: 0,
//             per_page: 100,
//             total: 0,
//         })
//     }

//     fn try_next(&mut self) -> Result<Option<Dependency>> {
//         if let Some(dep) = self.dependencies.next() {
//             return Ok(Some(dep));
//         }

//         if self.page > 0 && self.page * self.per_page >= self.total
// {             return Ok(None);
//         }

//         self.page += 1;
//         let url = format!(
//             "https://crates.io/api/v1/crates/{}/reverse_dependencies?page={}&per_page={}",
//             self.crate_id, self.page, self.per_page
//         );
//         println!("Calling {}", url);
//         let resp = self.client.get(url).send()?;
//         //println!("{:#?}", resp);
//         //let text = resp.text()?;
//         //println!("{:#?}", text);
//         let json = resp.json::<ApiResponse>()?;
//         self.dependencies = json.dependencies.into_iter();
//         self.total = json.meta.total;
//         Ok(self.dependencies.next())
//     }
// }

// impl Iterator for ReverseDependencies {
//     type Item = Result<Dependency>;

//     fn next(&mut self) -> Option<Self::Item> {
//         match self.try_next() {
//             Ok(Some(dep)) => Some(Ok(dep)),
//             Ok(None) => None,
//             Err(err) => Some(Err(err)),
//         }
//     }
// }

// fn main() -> Result<()> {
//     for dep in ReverseDependencies::of("bit-vec")? {
//         println!("reverse dependency: {}", dep?.crate_id);
//     }
//     Ok(())
// }

fn main() -> anyhow::Result<()> {
    Ok(())
}

Handle a rate-limited API

reqwest hyper cat-network-programming cat-web-programming cat-web-programming::http-client

This example uses the GitHub API - rate limiting⮳, as an example of how to handle remote server errors. This example uses the hyper::header!⮳ macro to parse the response header and checks for reqwest::StatusCode::FORBIDDEN⮳ If the response exceeds the rate limit, the example waits and retries.

// use std::thread;
// use std::time::Duration;
// use std::time::UNIX_EPOCH;

// use anyhow::anyhow;
// use anyhow::Result;
// use reqwest::StatusCode;

// // TODO fix interaction with https://docs.github.com/en/rest?apiVersion=2022-11-28

// fn main() -> Result<()> {
//     let url = "https://api.github.com/users/john-cd";
//     let client = reqwest::blocking::Client::new();

//     loop {
//         let response = client.get(url).send()?;
//         let hdrs = response.headers();

//         let rate_limit: usize =
// hdrs.get("x-ratelimit-limit").ok_or_else( || {
// anyhow!("response doesn't include the expected x-ratelimit-limit
// header") }         )?.to_str()?.parse()?;

//         let rate_remaining: usize =
// hdrs.get("x-ratelimit-remaining")         .ok_or_else(|| {
// anyhow!("response doesn't include the expected
// x-ratelimit-remaining header") })?.to_str()?.parse()?;

//         let rate_reset_at: u64 = hdrs.get("x-ratelimit-reset")
//         .ok_or_else(|| { anyhow!("response doesn't include the
// expected x-ratelimit-reset header") })?.to_str()?.parse()?;

//         let rate_reset_within =
//             Duration::from_secs(rate_reset_at) -
// UNIX_EPOCH.elapsed()?;

//         if response.status() == StatusCode::FORBIDDEN &&
// rate_remaining == 0 {             println!("Sleeping for {}
// seconds.", rate_reset_within.as_secs());
// thread::sleep(rate_reset_within);             continue;
//         } else {
//             println!(
//                 "Rate limit is currently {}/{}, the reset of this
// limit will be within {} seconds.",                 rate_remaining,
//                 rate_limit,
//                 rate_reset_within.as_secs(),
//             );
//             break;
//         }
//     }
//     Ok(())
// }

fn main() -> anyhow::Result<()> {
    Ok(())
}