wasmer_backend_api/
client.rs1#[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#[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 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 "" => 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 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}