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#[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: '{}'",
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 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}