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#[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 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 "" => 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 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}