wasmer_backend_api/
client.rs

1#[cfg(not(target_family = "wasm"))]
2use std::time::Duration;
3
4use crate::GraphQLApiFailure;
5use anyhow::{bail, Context as _};
6use cynic::{http::CynicReqwestError, GraphQlResponse, Operation};
7#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
8use reqwest::Proxy;
9use url::Url;
10
11#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
12pub struct Proxy;
13
14/// API client for the Wasmer API.
15///
16/// Use the queries in [`crate::queries`] to interact with the API.
17#[derive(Clone, Debug)]
18pub struct WasmerClient {
19    auth_token: Option<String>,
20    graphql_endpoint: Url,
21
22    pub(crate) client: reqwest::Client,
23    pub(crate) user_agent: reqwest::header::HeaderValue,
24    #[allow(unused)]
25    log_variables: bool,
26}
27
28impl WasmerClient {
29    /// Env var used to enable logging of request variables.
30    ///
31    /// This is somewhat dangerous since it can log sensitive information, hence
32    /// it is gated by a custom env var.
33    const ENV_VAR_LOG_VARIABLES: &'static str = "WASMER_API_INSECURE_LOG_VARIABLES";
34
35    pub fn graphql_endpoint(&self) -> &Url {
36        &self.graphql_endpoint
37    }
38
39    pub fn auth_token(&self) -> Option<&str> {
40        self.auth_token.as_deref()
41    }
42
43    fn parse_user_agent(user_agent: &str) -> Result<reqwest::header::HeaderValue, anyhow::Error> {
44        if user_agent.is_empty() {
45            bail!("user agent must not be empty");
46        }
47        user_agent
48            .parse()
49            .with_context(|| format!("invalid user agent: '{user_agent}'"))
50    }
51
52    pub fn new_with_client(
53        client: reqwest::Client,
54        graphql_endpoint: Url,
55        user_agent: &str,
56    ) -> Result<Self, anyhow::Error> {
57        let log_variables = {
58            let v = std::env::var(Self::ENV_VAR_LOG_VARIABLES).unwrap_or_default();
59            match v.as_str() {
60                "1" | "true" => true,
61                "0" | "false" => false,
62                // Default case if not provided.
63                "" => false,
64                other => {
65                    bail!(
66                        "invalid value for {} - expected 0/false|1/true: '{}'",
67                        Self::ENV_VAR_LOG_VARIABLES,
68                        other
69                    );
70                }
71            }
72        };
73
74        Ok(Self {
75            client,
76            auth_token: None,
77            user_agent: Self::parse_user_agent(user_agent)?,
78            graphql_endpoint,
79            log_variables,
80        })
81    }
82
83    pub fn new(graphql_endpoint: Url, user_agent: &str) -> Result<Self, anyhow::Error> {
84        Self::new_with_proxy(graphql_endpoint, user_agent, None)
85    }
86
87    #[cfg_attr(all(target_arch = "wasm32", target_os = "unknown"), allow(unused))]
88    pub fn new_with_proxy(
89        graphql_endpoint: Url,
90        user_agent: &str,
91        proxy: Option<Proxy>,
92    ) -> Result<Self, anyhow::Error> {
93        let builder = {
94            #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
95            let builder = reqwest::ClientBuilder::new();
96
97            #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
98            let builder = reqwest::ClientBuilder::new()
99                .connect_timeout(Duration::from_secs(10))
100                .timeout(Duration::from_secs(90));
101
102            #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
103            if let Some(proxy) = proxy {
104                builder.proxy(proxy)
105            } else {
106                builder
107            }
108            #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
109            builder
110        };
111
112        let client = builder.build().context("failed to create reqwest client")?;
113
114        Self::new_with_client(client, graphql_endpoint, user_agent)
115    }
116
117    pub fn with_auth_token(mut self, auth_token: String) -> Self {
118        self.auth_token = Some(auth_token);
119        self
120    }
121
122    pub(crate) async fn run_graphql_raw<ResponseData, Vars>(
123        &self,
124        operation: Operation<ResponseData, Vars>,
125    ) -> Result<cynic::GraphQlResponse<ResponseData>, anyhow::Error>
126    where
127        Vars: serde::Serialize + std::fmt::Debug,
128        ResponseData: serde::de::DeserializeOwned + std::fmt::Debug + 'static,
129    {
130        let req = self
131            .client
132            .post(self.graphql_endpoint.as_str())
133            .header(reqwest::header::USER_AGENT, &self.user_agent);
134        let req = if let Some(token) = &self.auth_token {
135            req.bearer_auth(token)
136        } else {
137            req
138        };
139
140        let query = operation.query.clone();
141
142        if self.log_variables {
143            tracing::trace!(
144                endpoint=%self.graphql_endpoint,
145                query=serde_json::to_string(&operation).unwrap_or_default(),
146                vars=?operation.variables,
147                "sending graphql query"
148            );
149        } else {
150            tracing::trace!(
151                endpoint=%self.graphql_endpoint,
152                query=serde_json::to_string(&operation).unwrap_or_default(),
153                "sending graphql query"
154            );
155        }
156
157        let res = req.json(&operation).send().await;
158
159        let res = match res {
160            Ok(response) => {
161                let status = response.status();
162                if !status.is_success() {
163                    let body_string = match response.text().await {
164                        Ok(b) => b,
165                        Err(err) => {
166                            tracing::error!("could not load response body: {err}");
167                            "<could not retrieve body>".to_string()
168                        }
169                    };
170
171                    match serde_json::from_str::<GraphQlResponse<ResponseData>>(&body_string) {
172                        Ok(response) => Ok(response),
173                        Err(_) => Err(CynicReqwestError::ErrorResponse(status, body_string)),
174                    }
175                } else {
176                    let body = response.bytes().await?;
177
178                    let jd = &mut serde_json::Deserializer::from_slice(&body);
179                    let data: Result<GraphQlResponse<ResponseData>, _> =
180                        serde_path_to_error::deserialize(jd).map_err(|err| {
181                            let body_txt = String::from_utf8_lossy(&body);
182                            CynicReqwestError::ErrorResponse(
183                                reqwest::StatusCode::INTERNAL_SERVER_ERROR,
184                                format!("Could not decode JSON response: {err} -- '{body_txt}'"),
185                            )
186                        });
187
188                    data
189                }
190            }
191            Err(e) => Err(CynicReqwestError::ReqwestError(e)),
192        };
193        let res = match res {
194            Ok(res) => {
195                tracing::trace!(?res, "GraphQL query succeeded");
196                res
197            }
198            Err(err) => {
199                tracing::error!(?err, "GraphQL query failed");
200                return Err(err.into());
201            }
202        };
203
204        if let Some(errors) = &res.errors {
205            if !errors.is_empty() {
206                tracing::warn!(
207                    ?errors,
208                    data=?res.data,
209                    %query,
210                    endpoint=%self.graphql_endpoint,
211                    "GraphQL query succeeded, but returned errors",
212                );
213            }
214        }
215
216        Ok(res)
217    }
218
219    pub(crate) async fn run_graphql<ResponseData, Vars>(
220        &self,
221        operation: Operation<ResponseData, Vars>,
222    ) -> Result<ResponseData, anyhow::Error>
223    where
224        Vars: serde::Serialize + std::fmt::Debug,
225        ResponseData: serde::de::DeserializeOwned + std::fmt::Debug + 'static,
226    {
227        let res = self.run_graphql_raw(operation).await?;
228
229        if let Some(data) = res.data {
230            Ok(data)
231        } else if let Some(errs) = res.errors {
232            let errs = GraphQLApiFailure { errors: errs };
233            Err(errs).context("GraphQL query failed")
234        } else {
235            Err(anyhow::anyhow!("Query did not return any data"))
236        }
237    }
238
239    /// Run a GraphQL query, but fail (return an Error) if any error is returned
240    /// in the response.
241    pub(crate) async fn run_graphql_strict<ResponseData, Vars>(
242        &self,
243        operation: Operation<ResponseData, Vars>,
244    ) -> Result<ResponseData, anyhow::Error>
245    where
246        Vars: serde::Serialize + std::fmt::Debug,
247        ResponseData: serde::de::DeserializeOwned + std::fmt::Debug + 'static,
248    {
249        let res = self.run_graphql_raw(operation).await?;
250
251        if let Some(errs) = res.errors {
252            if !errs.is_empty() {
253                let errs = GraphQLApiFailure { errors: errs };
254                return Err(errs).context("GraphQL query failed");
255            }
256        }
257
258        if let Some(data) = res.data {
259            Ok(data)
260        } else {
261            Err(anyhow::anyhow!("Query did not return any data"))
262        }
263    }
264}