wasmer_backend_api/
client.rs

1#[cfg(not(target_family = "wasm"))]
2use std::time::Duration;
3
4use crate::GraphQLApiFailure;
5use anyhow::{Context as _, bail};
6use cynic::{GraphQlResponse, Operation, http::CynicReqwestError};
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::query`] 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: '{other}'",
67                        Self::ENV_VAR_LOG_VARIABLES
68                    );
69                }
70            }
71        };
72
73        Ok(Self {
74            client,
75            auth_token: None,
76            user_agent: Self::parse_user_agent(user_agent)?,
77            graphql_endpoint,
78            log_variables,
79        })
80    }
81
82    pub fn new(graphql_endpoint: Url, user_agent: &str) -> Result<Self, anyhow::Error> {
83        Self::new_with_proxy(graphql_endpoint, user_agent, None)
84    }
85
86    #[cfg_attr(all(target_arch = "wasm32", target_os = "unknown"), allow(unused))]
87    pub fn new_with_proxy(
88        graphql_endpoint: Url,
89        user_agent: &str,
90        proxy: Option<Proxy>,
91    ) -> Result<Self, anyhow::Error> {
92        let builder = {
93            #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
94            let builder = reqwest::ClientBuilder::new();
95
96            #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
97            let builder = reqwest::ClientBuilder::new()
98                .connect_timeout(Duration::from_secs(10))
99                .timeout(Duration::from_secs(90));
100
101            #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
102            if let Some(proxy) = proxy {
103                builder.proxy(proxy)
104            } else {
105                builder
106            }
107            #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
108            builder
109        };
110
111        let client = builder.build().context("failed to create reqwest client")?;
112
113        Self::new_with_client(client, graphql_endpoint, user_agent)
114    }
115
116    pub fn with_auth_token(mut self, auth_token: String) -> Self {
117        self.auth_token = Some(auth_token);
118        self
119    }
120
121    pub(crate) async fn run_graphql_raw<ResponseData, Vars>(
122        &self,
123        operation: Operation<ResponseData, Vars>,
124    ) -> Result<cynic::GraphQlResponse<ResponseData>, anyhow::Error>
125    where
126        Vars: serde::Serialize + std::fmt::Debug,
127        ResponseData: serde::de::DeserializeOwned + std::fmt::Debug + 'static,
128    {
129        let req = self
130            .client
131            .post(self.graphql_endpoint.as_str())
132            .header(reqwest::header::USER_AGENT, &self.user_agent);
133        let req = if let Some(token) = &self.auth_token {
134            req.bearer_auth(token)
135        } else {
136            req
137        };
138
139        let query = operation.query.clone();
140
141        if self.log_variables {
142            tracing::trace!(
143                endpoint=%self.graphql_endpoint,
144                query=serde_json::to_string(&operation).unwrap_or_default(),
145                vars=?operation.variables,
146                "sending graphql query"
147            );
148        } else {
149            tracing::trace!(
150                endpoint=%self.graphql_endpoint,
151                query=serde_json::to_string(&operation).unwrap_or_default(),
152                "sending graphql query"
153            );
154        }
155
156        let res = req.json(&operation).send().await;
157
158        let res = match res {
159            Ok(response) => {
160                let status = response.status();
161                if !status.is_success() {
162                    let body_string = match response.text().await {
163                        Ok(b) => b,
164                        Err(err) => {
165                            tracing::error!("could not load response body: {err}");
166                            "<could not retrieve body>".to_string()
167                        }
168                    };
169
170                    match serde_json::from_str::<GraphQlResponse<ResponseData>>(&body_string) {
171                        Ok(response) => Ok(response),
172                        Err(_) => Err(CynicReqwestError::ErrorResponse(status, body_string)),
173                    }
174                } else {
175                    let body = response.bytes().await?;
176
177                    let jd = &mut serde_json::Deserializer::from_slice(&body);
178                    let data: Result<GraphQlResponse<ResponseData>, _> =
179                        serde_path_to_error::deserialize(jd).map_err(|err| {
180                            let body_txt = String::from_utf8_lossy(&body);
181                            CynicReqwestError::ErrorResponse(
182                                reqwest::StatusCode::INTERNAL_SERVER_ERROR,
183                                format!("Could not decode JSON response: {err} -- '{body_txt}'"),
184                            )
185                        });
186
187                    data
188                }
189            }
190            Err(e) => Err(CynicReqwestError::ReqwestError(e)),
191        };
192        let res = match res {
193            Ok(res) => {
194                tracing::trace!(?res, "GraphQL query succeeded");
195                res
196            }
197            Err(err) => {
198                tracing::error!(?err, "GraphQL query failed");
199                return Err(err.into());
200            }
201        };
202
203        if let Some(errors) = &res.errors {
204            if !errors.is_empty() {
205                tracing::warn!(
206                    ?errors,
207                    data=?res.data,
208                    %query,
209                    endpoint=%self.graphql_endpoint,
210                    "GraphQL query succeeded, but returned errors",
211                );
212            }
213        }
214
215        Ok(res)
216    }
217
218    pub(crate) async fn run_graphql<ResponseData, Vars>(
219        &self,
220        operation: Operation<ResponseData, Vars>,
221    ) -> Result<ResponseData, anyhow::Error>
222    where
223        Vars: serde::Serialize + std::fmt::Debug,
224        ResponseData: serde::de::DeserializeOwned + std::fmt::Debug + 'static,
225    {
226        let res = self.run_graphql_raw(operation).await?;
227
228        if let Some(data) = res.data {
229            Ok(data)
230        } else if let Some(errs) = res.errors {
231            let errs = GraphQLApiFailure { errors: errs };
232            Err(errs).context("GraphQL query failed")
233        } else {
234            Err(anyhow::anyhow!("Query did not return any data"))
235        }
236    }
237
238    /// Run a GraphQL query, but fail (return an Error) if any error is returned
239    /// in the response.
240    pub(crate) async fn run_graphql_strict<ResponseData, Vars>(
241        &self,
242        operation: Operation<ResponseData, Vars>,
243    ) -> Result<ResponseData, anyhow::Error>
244    where
245        Vars: serde::Serialize + std::fmt::Debug,
246        ResponseData: serde::de::DeserializeOwned + std::fmt::Debug + 'static,
247    {
248        let res = self.run_graphql_raw(operation).await?;
249
250        if let Some(errs) = res.errors {
251            if !errs.is_empty() {
252                let errs = GraphQLApiFailure { errors: errs };
253                return Err(errs).context("GraphQL query failed");
254            }
255        }
256
257        if let Some(data) = res.data {
258            Ok(data)
259        } else {
260            Err(anyhow::anyhow!("Query did not return any data"))
261        }
262    }
263}