boardgamegeek/
protocol.rs

1use crate::result::{Error, Result};
2use backoff::{future, ExponentialBackoff};
3use log::warn;
4use reqwest::StatusCode;
5
6const LOG_TARGET: &str = "boardgamegeek::Client";
7
8// This "base" Client exists to handle the random idiosyncracies of BGG.
9pub struct Client {
10    http_client: reqwest::Client,
11    no_redirect_client: reqwest::Client,
12}
13
14async fn collect(response: reqwest::Response) -> Result<String> {
15    match response.text().await {
16        Ok(text) => Ok(text),
17        Err(_err) => Err(Error::BadResponse),
18    }
19}
20
21impl Client {
22    pub fn new() -> Self {
23        Self {
24            http_client: reqwest::Client::builder().build().unwrap(),
25            no_redirect_client: reqwest::Client::builder()
26                .redirect(reqwest::redirect::Policy::none())
27                .build()
28                .unwrap(),
29        }
30    }
31
32    pub async fn get(&self, url: &str) -> Result<String> {
33        future::retry(ExponentialBackoff::default(), || async {
34            let result = self.http_client.get(url).send().await;
35
36            if let Err(_err) = result {
37                return Err(backoff::Error::Permanent(Error::ConnectionFailed));
38            }
39
40            let response = result.unwrap();
41            match response.status() {
42                StatusCode::TOO_MANY_REQUESTS => {
43                    warn!(target: LOG_TARGET, "Received 429: {:?}", response);
44                    Err(backoff::Error::Transient(Error::TooManyRequests))
45                }
46                StatusCode::OK => Ok(collect(response).await?),
47                code => Err(backoff::Error::Permanent(Error::RequestFailed(
48                    code.as_u16(),
49                ))),
50            }
51        })
52        .await
53    }
54
55    pub async fn get_redirect_location(&self, url: &str) -> Result<Option<String>> {
56        let result = self.no_redirect_client.get(url).send().await;
57
58        if let Err(_err) = result {
59            return Err(Error::ConnectionFailed);
60        }
61
62        let response = result.unwrap();
63        match response.status() {
64            StatusCode::FOUND => match response.headers().get("Location") {
65                Some(v) => Ok(Some(v.to_str().unwrap().to_owned())),
66                None => Ok(None),
67            },
68            code => Err(Error::RequestFailed(code.as_u16())),
69        }
70    }
71
72    pub async fn get_with_202_check(&self, url: &str) -> Result<String> {
73        future::retry(backoff::ExponentialBackoff::default(), || async {
74            match self.get(url).await {
75                Err(Error::RequestFailed(202)) => {
76                    warn!(target: LOG_TARGET, "Received 202. Retrying...");
77                    Err(backoff::Error::Transient(Error::RequestFailed(202)))
78                }
79
80                Err(e) => Err(backoff::Error::Permanent(e)),
81                Ok(r) => Ok(r),
82            }
83        })
84        .await
85    }
86}