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

Calling a Web API

Query the GitHub API

reqwest serde Net Encoding

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

The program expects the GitHub personal access token to be specified in the environment variable GITHUB_TOKEN and sends it with RequestBuilder::bearer_auth. Request setup includes the reqwest::header::USER_AGENT header as required by the GitHub API. The program deserializes the response body with Response::json into a vector of User objects.

This recipe requires the blocking and json feature flags of reqwest and the derive feature flag of serde to be enabled in Cargo.toml.

use reqwest::header::USER_AGENT;
use serde::Deserialize;
use std::env;

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let request_url = format!(
        "https://api.github.com/repos/{owner}/{repo}/stargazers",
        owner = "rust-lang-nursery",
        repo = "rust-cookbook"
    );
    println!("{}", request_url);

    let token = env::var("GITHUB_TOKEN")?;

    let client = reqwest::blocking::Client::new();
    let response = client
        .get(request_url)
        .header(USER_AGENT, "rust-web-api-client") // gh api requires a user-agent header
        .bearer_auth(token)
        .send()?;

    let users: Vec<User> = response.json()?;
    for user in users {
        println!("{} ({})", user.login, user.id);
    }
    Ok(())
}

Check if an API resource exists

reqwest Net

Query the GitHub Users Endpoint using a HEAD request (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 ClientBuilder::timeout ensures a request will not last longer than a timeout. The client also sets a ClientBuilder::user_agent, which the GitHub API requires on every request.

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

This recipe requires the blocking feature flag of reqwest to be enabled in Cargo.toml.

use reqwest::Result;
use std::time::Duration;

fn main() -> 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 = reqwest::blocking::ClientBuilder::new()
        .timeout(timeout)
        .user_agent("rust-web-api-client") // gh api requires a user-agent header
        .build()?;
    let response = client.head(&request_url).send()?;

    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 serde_json Net Encoding

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

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

This recipe requires the blocking and json feature flags of reqwest and the derive feature flag of serde to be enabled in Cargo.toml.

use anyhow::Result;
use serde::Deserialize;
use serde_json::json;
use std::env;

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

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

    let gist_body = json!({
    "description": "the description for this gist",
    "public": true,
    "files": {
         "main.rs": {
         "content": r#"fn main() { println!("hello world!");}"#
        }
    }});

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

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

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

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

This recipe 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 Net Encoding

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.

This recipe requires the blocking and json feature flags of reqwest and the derive feature flag of serde to be enabled in Cargo.toml.

#![allow(unused)]
fn main() {
use reqwest::Result;
use reqwest::header::USER_AGENT;
use serde::Deserialize;

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

#[derive(Deserialize)]
pub struct Dependency {
    pub crate_id: String,
    pub id: u32,
}

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

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

impl ReverseDependencies {
    pub fn of(crate_id: &str) -> Result<Self> {
        Ok(ReverseDependencies {
            crate_id: crate_id.to_owned(),
            dependencies: vec![].into_iter(),
            client: reqwest::blocking::Client::new(),
            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!("{}", url);

        let response = self
            .client
            .get(&url)
            .header(USER_AGENT, "cookbook agent")
            .send()?
            .json::<ApiResponse>()?;
        self.dependencies = response.dependencies.into_iter();
        self.total = response.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)),
        }
    }
}
}
use web::paginated;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    for dep in paginated::ReverseDependencies::of("serde")? {
        let dependency = dep?;
        println!("{} depends on {}", dependency.id, dependency.crate_id);
    }
    Ok(())
}