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