rspotify_http/
ureq.rs

1//! The client implementation for the ureq HTTP client, which is blocking.
2
3use super::{BaseHttpClient, Form, Headers, Query};
4
5use std::{io, time::Duration};
6
7use maybe_async::sync_impl;
8use serde_json::Value;
9use ureq::{Request, Response};
10
11/// Custom enum that contains all the possible errors that may occur when using
12/// `ureq`.
13///
14/// Sample usage:
15///
16/// ```
17/// use rspotify_http::{HttpError, HttpClient, BaseHttpClient};
18///
19/// let client = HttpClient::default();
20/// let response = client.get("wrongurl", None, &Default::default());
21/// match response {
22///     Ok(data) => println!("request succeeded: {:?}", data),
23///     Err(HttpError::Transport(e)) => eprintln!("request failed: {}", e),
24///     Err(HttpError::Io(e)) => eprintln!("failed to decode response: {}", e),
25///     Err(HttpError::StatusCode(response)) => {
26///         let code = response.status();
27///         match response.into_json::<rspotify_model::ApiError>() {
28///             Ok(api_error) => eprintln!("status code {}: {:?}", code, api_error),
29///             Err(_) => eprintln!("status code {}", code),
30///         }
31///     },
32/// }
33/// ```
34#[derive(thiserror::Error, Debug)]
35pub enum UreqError {
36    /// The request couldn't be completed because there was an error when trying
37    /// to do so
38    #[error("transport: {0}")]
39    Transport(#[from] ureq::Transport),
40
41    /// There was an error when trying to decode the response
42    #[error("I/O: {0}")]
43    Io(#[from] io::Error),
44
45    /// The request was made, but the server returned an unsuccessful status
46    /// code, such as 404 or 503. In some cases, the response may contain a
47    /// custom message from Spotify with more information, which can be
48    /// serialized into `rspotify_model::ApiError`.
49    #[error("status code {}", ureq::Response::status(.0))]
50    StatusCode(ureq::Response),
51}
52
53#[derive(Debug, Clone)]
54pub struct UreqClient {
55    agent: ureq::Agent,
56}
57
58impl Default for UreqClient {
59    fn default() -> Self {
60        let agent = ureq::AgentBuilder::new()
61            .try_proxy_from_env(true)
62            .timeout(Duration::from_secs(10));
63
64        #[cfg(feature = "ureq-native-tls")]
65        let agent = agent.tls_connector(std::sync::Arc::new(
66            native_tls::TlsConnector::builder()
67                // rust-native-tls defaults to a minimum of TLS 1.0, which is insecure
68                .min_protocol_version(Some(native_tls::Protocol::Tlsv12))
69                .build()
70                .expect("Failed to initialize TLS connector"),
71        ));
72
73        Self {
74            agent: agent.build(),
75        }
76    }
77}
78
79impl UreqClient {
80    /// The request handling in ureq is split in three parts:
81    ///
82    /// * The initial request (POST, GET, ...) is given as the `request`
83    ///   parameter.
84    /// * This method will add whichever headers and additional data is needed
85    ///   for all requests.
86    /// * The request is finished and performed with the `send_request` function
87    ///   (JSON, a form...).
88    fn request<D>(
89        &self,
90        mut request: Request,
91        headers: Option<&Headers>,
92        send_request: D,
93    ) -> Result<String, UreqError>
94    where
95        D: Fn(Request) -> Result<Response, ureq::Error>,
96    {
97        // Setting the headers, which will be the token auth if unspecified.
98        if let Some(headers) = headers {
99            for (key, val) in headers.iter() {
100                request = request.set(key, val);
101            }
102        }
103
104        log::info!("Making request {request:#?}");
105        // Converting errors from ureq into our custom error types
106        match send_request(request) {
107            Ok(response) => response.into_string().map_err(Into::into),
108            Err(err) => match err {
109                ureq::Error::Status(_, response) => Err(UreqError::StatusCode(response)),
110                ureq::Error::Transport(transport) => Err(UreqError::Transport(transport)),
111            },
112        }
113    }
114}
115
116#[sync_impl]
117impl BaseHttpClient for UreqClient {
118    type Error = UreqError;
119
120    #[inline]
121    fn get(
122        &self,
123        url: &str,
124        headers: Option<&Headers>,
125        payload: &Query,
126    ) -> Result<String, Self::Error> {
127        let request = self.agent.get(url);
128        let sender = |mut req: Request| {
129            for (key, val) in payload.iter() {
130                req = req.query(key, val);
131            }
132            req.call()
133        };
134        self.request(request, headers, sender)
135    }
136
137    #[inline]
138    fn post(
139        &self,
140        url: &str,
141        headers: Option<&Headers>,
142        payload: &Value,
143    ) -> Result<String, Self::Error> {
144        let request = self.agent.post(url);
145        let sender = |req: Request| req.send_json(payload.clone());
146        self.request(request, headers, sender)
147    }
148
149    #[inline]
150    fn post_form(
151        &self,
152        url: &str,
153        headers: Option<&Headers>,
154        payload: &Form<'_>,
155    ) -> Result<String, Self::Error> {
156        let request = self.agent.post(url);
157        let sender = |req: Request| {
158            let payload = payload
159                .iter()
160                .map(|(key, val)| (*key, *val))
161                .collect::<Vec<_>>();
162
163            req.send_form(&payload)
164        };
165
166        self.request(request, headers, sender)
167    }
168
169    #[inline]
170    fn put(
171        &self,
172        url: &str,
173        headers: Option<&Headers>,
174        payload: &Value,
175    ) -> Result<String, Self::Error> {
176        let request = self.agent.put(url);
177        let sender = |req: Request| req.send_json(payload.clone());
178        self.request(request, headers, sender)
179    }
180
181    #[inline]
182    fn delete(
183        &self,
184        url: &str,
185        headers: Option<&Headers>,
186        payload: &Value,
187    ) -> Result<String, Self::Error> {
188        let request = self.agent.delete(url);
189        let sender = |req: Request| req.send_json(payload.clone());
190        self.request(request, headers, sender)
191    }
192}