Calling a Web API
Recipe | Crates | Categories |
---|---|---|
Query the GitHub API | ||
Check if an API resource exists | ||
Create and delete Gist with GitHub API | ||
Consume a paginated RESTful API | ||
Handle a rate-limited API |
Query the GitHub API
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
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
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
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
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(()) }