Reqwest request not getting my reqwest::Client's default headers

1.5k Views Asked by At

I am trying to write a library that communicates with Todoist's REST API. The idea is that the library exposes a TodoistAPI struct that contains a reqwest::Client and a base_url. There is a new() function that returns the an instantiated TodoistAPI struct with a client that has a bearer token (supplied by a program utilizing my library) in its default headers.

However, I am running into an issue where when it comes time to actually use the Client to make an API request, the default headers are not getting set at all.

The TodoistAPI struct, new method, and get_projects method.

#[derive(Debug)]
pub struct TodoistAPI{
    base_url: Url,
    client: reqwest::Client
}

impl TodoistAPI {
    #[allow(dead_code)]
    pub fn new(token: &str) -> Result<TodoistAPI, TodoistAPIError> {
        let mut headers = header::HeaderMap::new();
        let header_token_value = header::HeaderValue::from_str(token).map_err(TodoistAPIError::InvalidHeaderValue)?;
        headers.insert(header::HeaderName::from_bytes(b"Bearer").map_err(TodoistAPIError::InvalidHeaderName)?, header_token_value);
        let client = reqwest::Client::builder()
            .default_headers(headers)
            .build().map_err(TodoistAPIError::Error)?;
        println!("{:#?}", client);
        let base_url = Url::parse(BASE_URL).map_err(TodoistAPIError::UrlParseError)?;
        return Ok(TodoistAPI{ base_url, client })
    }

    #[allow(dead_code)]
    pub async fn get_projects(&self) -> Result<Vec<Project>, TodoistAPIError> {
        let url = self.base_url.join("projects").map_err(TodoistAPIError::UrlParseError)?;
        let request_builder = self.client.request(reqwest::Method::GET, url);
        println!("{:#?}", request_builder);
        let request = request_builder.build().map_err(TodoistAPIError::Error)?;
        println!("{:#?}", request);
        let response = self.client.execute(request).await.map_err(TodoistAPIError::Error)?;
        println!("Status: {}", response.status());
        println!("STatus: {:#?}", response.text().await.map_err(TodoistAPIError::Error)?);
        let url = self.base_url.join("projects").map_err(TodoistAPIError::UrlParseError)?;
        let projects = self.client.get(url)
            .send()
            .await.map_err(TodoistAPIError::Error)?
            .json::<Vec<Project>>()
            .await.map_err(TodoistAPIError::Error)?;
        return Ok(projects);
    }
}

A small CLI program that gets token from environment variable and calls the get_projects method.

use structopt::StructOpt;
use oxidoist_api::TodoistAPI;
use oxidoist_api::Project;
use oxidoist_api::TodoistAPIError;

use std::env;

#[derive(StructOpt, Debug)]
struct Cli {
    verb: String, //get, add, complete, etc.
    datatype: String, //project, task, section, etc.
}

#[tokio::main]
async fn main() -> Result<(), TodoistAPIError> {
    let args = Cli::from_args();
    let token = env::var("TODOIST_API_KEY").unwrap();
    let todoist_api_object = TodoistAPI::new(token.as_str()).unwrap();
    if args.verb == "get" {
        if args.datatype == "projects" {
            let projects: Vec<Project> = todoist_api_object.get_projects().await?;
            println!("{:?}", projects);
        }
    }
    
    Ok(())
}

The println! statements result in the following output (with some obviously redacted private information).

Client {
    accepts: Accepts,
    proxies: [
        Proxy(
            System(
                {},
            ),
            None,
        ),
    ],
    referer: true,
    default_headers: {
        "accept": "*/*",
        "bearer": "REDACTED",
    },
}
RequestBuilder {
    method: GET,
    url: Url {
        scheme: "https",
        host: Some(
            Domain(
                "api.todoist.com",
            ),
        ),
        port: None,
        path: "/rest/v1/projects",
        query: None,
        fragment: None,
    },
    headers: {},
}
Request {
    method: GET,
    url: Url {
        scheme: "https",
        host: Some(
            Domain(
                "api.todoist.com",
            ),
        ),
        port: None,
        path: "/rest/v1/projects",
        query: None,
        fragment: None,
    },
    headers: {},
}
Status: 400 Bad Request
STatus: "Empty token\n"
Error: Error(reqwest::Error { kind: Decode, source: Error("expected value", line: 1, column: 1) })

I'm truly stumped here. Everything I am reading says that I am doing it right, but the default headers are definitely NOT getting added to requests spawned from the Client.

1

There are 1 best solutions below

0
On

So it turns out I was misunderstanding of a couple of things. First, the headers. Turns out I was just putting them in incorrectly. The header name needed to be b"Authorization", and the value needed to be "Bearer <token>". I have changed the code to be the following:

let mut token: String = "Bearer ".to_string();
token.push_str(&self.token);
let header_token_value = header::HeaderValue::from_str(&token).map_err(TodoistAPIError::InvalidHeaderValue)?;

Secondly, even after getting it to work correctly, the headers: {} entry in the Request and RequestBuilder printouts was still blank, so I guess reqwest merges them with the Client's default_headers when it actually sends the request.

There may be better ways to handle the Bearer token thing, it feels a bit clumsy. If anyone finding this knows of any feel free to leave a better answer.