modio/
request.rs

1use futures_util::TryFutureExt;
2use reqwest::header::{HeaderValue, CONTENT_TYPE};
3use reqwest::multipart::Form;
4use reqwest::StatusCode;
5use serde::de::DeserializeOwned;
6use serde::ser::Serialize;
7use tracing::{debug, level_enabled, trace};
8use url::Url;
9
10use crate::auth::Token;
11use crate::error::{self, Result};
12use crate::routing::{Parts, Route};
13use crate::types::ErrorResponse;
14use crate::Modio;
15
16#[allow(dead_code)]
17mod headers {
18    const X_MODIO_ERROR_REF: &str = "x-modio-error-ref";
19    const X_MODIO_REQUEST_ID: &str = "x-modio-request-id";
20
21    use http::header::{HeaderMap, RETRY_AFTER};
22
23    pub fn retry_after(headers: &HeaderMap) -> Option<u64> {
24        headers
25            .get(RETRY_AFTER)
26            .and_then(|v| v.to_str().ok())
27            .and_then(|v| v.parse().ok())
28    }
29}
30
31pub struct RequestBuilder {
32    modio: Modio,
33    request: Result<reqwest::RequestBuilder>,
34}
35
36impl RequestBuilder {
37    pub fn new(modio: Modio, route: Route) -> Self {
38        let Parts {
39            method,
40            path,
41            token_required,
42        } = route.into_parts();
43
44        if let (true, None) = (token_required, &modio.inner.credentials.token) {
45            return Self {
46                modio,
47                request: Err(error::token_required()),
48            };
49        }
50
51        let url = format!("{}{}", modio.inner.host, path);
52        let params = [("api_key", &modio.inner.credentials.api_key)];
53        let request = Url::parse_with_params(&url, &params)
54            .map(|url| {
55                let mut req = modio.inner.client.request(method, url);
56
57                if let (true, Some(Token { value, .. })) =
58                    (token_required, &modio.inner.credentials.token)
59                {
60                    req = req.bearer_auth(value);
61                }
62                req
63            })
64            .map_err(error::builder);
65
66        Self { modio, request }
67    }
68
69    pub fn query<T: Serialize + ?Sized>(self, query: &T) -> Self {
70        Self {
71            request: self.request.map(|r| r.query(query)),
72            ..self
73        }
74    }
75
76    pub fn form<T: Serialize + ?Sized>(self, form: &T) -> Self {
77        Self {
78            request: self.request.map(|r| r.form(form)),
79            ..self
80        }
81    }
82
83    pub fn multipart(self, form: Form) -> Self {
84        Self {
85            request: self.request.map(|r| r.multipart(form)),
86            ..self
87        }
88    }
89
90    pub async fn send<Out>(self) -> Result<Out>
91    where
92        Out: DeserializeOwned + Send,
93    {
94        let mut req = self.request?.build().map_err(error::builder)?;
95        if !req.headers().contains_key(CONTENT_TYPE) {
96            req.headers_mut().insert(
97                CONTENT_TYPE,
98                HeaderValue::from_static("application/x-www-form-urlencoded"),
99            );
100        }
101
102        debug!("request: {} {}", req.method(), req.url());
103        let response = self
104            .modio
105            .inner
106            .client
107            .execute(req)
108            .map_err(error::request)
109            .await?;
110
111        let status = response.status();
112
113        let retry_after = if status.is_success() {
114            None
115        } else {
116            headers::retry_after(response.headers())
117        };
118
119        trace!("response headers: {:?}", response.headers());
120
121        let body = response.bytes().map_err(error::request).await?;
122
123        if level_enabled!(tracing::Level::TRACE) {
124            match std::str::from_utf8(&body) {
125                Ok(s) => trace!("status: {}, response: {}", status, s),
126                Err(_) => trace!("status: {}, response: {:?}", status, body),
127            }
128        }
129
130        if status == StatusCode::NO_CONTENT {
131            serde_json::from_str("null").map_err(error::decode)
132        } else if status.is_success() {
133            serde_json::from_slice(&body).map_err(error::decode)
134        } else if let Some(retry_after) = retry_after {
135            debug!("ratelimit reached: retry after {retry_after} seconds");
136            Err(error::ratelimit(retry_after))
137        } else {
138            serde_json::from_slice::<ErrorResponse>(&body)
139                .map(|mer| Err(error::error_for_status(status, mer.error)))
140                .map_err(error::decode)?
141        }
142    }
143}