1use std::{
6 num::{ParseIntError, TryFromIntError},
7 string::FromUtf8Error,
8};
9
10use cynic::serde::Deserialize;
11use iota_types::{
12 AddressParseError, DigestParseError, TypeParseError, TypeTag, iota_names::error::IotaNamesError,
13};
14use reqwest::{StatusCode, Url};
15
16use crate::faucet::FaucetError;
17
18type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
19
20pub type GraphQLResult<T> = std::result::Result<T, GraphQLError>;
21
22const MAX_ERROR_BODY_BYTES: usize = 512;
26
27fn truncated_body(bytes: &[u8]) -> String {
30 let truncated = bytes.len() > MAX_ERROR_BODY_BYTES;
31 let slice = &bytes[..bytes.len().min(MAX_ERROR_BODY_BYTES)];
32 let mut body = String::from_utf8_lossy(slice).into_owned();
33 if truncated {
34 body.push_str("… (truncated)");
35 }
36 body
37}
38
39pub type QueryError = cynic::GraphQlError<ErrorExtensions>;
42
43#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
45#[serde(crate = "cynic::serde")]
46#[non_exhaustive]
47pub struct ErrorExtensions {
48 pub code: Option<String>,
50}
51
52pub(crate) fn query_error(error: cynic::GraphQlError) -> QueryError {
54 QueryError::new(error.message, error.locations, error.path, None)
55}
56
57fn display_graphql_errors(errors: &[QueryError]) -> String {
58 errors
59 .iter()
60 .map(|e| e.to_string())
61 .collect::<Vec<_>>()
62 .join(", ")
63}
64
65#[derive(Debug, thiserror::Error)]
74#[non_exhaustive]
75pub enum GraphQLError {
76 #[error("request error: {0}")]
78 Request(#[from] reqwest::Error),
79 #[error(
81 "GraphQL request to {url} failed with HTTP {status} while decoding `{target_type}`, \
82 body={body:?}",
83 url = .response.url,
84 status = .response.status,
85 target_type = .response.target_type,
86 body = .response.body,
87 )]
88 Http { response: Box<HttpResponse> },
89 #[error(
91 "GraphQL request to {url} returned HTTP {status} but the body could not be parsed as JSON \
92 while decoding `{target_type}` (body={body:?}): {source}",
93 url = .response.url,
94 status = .response.status,
95 target_type = .response.target_type,
96 body = .response.body,
97 )]
98 Json {
99 response: Box<HttpResponse>,
100 #[source]
101 source: serde_json::Error,
102 },
103 #[error("query error: [{}]", display_graphql_errors(.0))]
106 Query(Vec<QueryError>),
107 #[error("empty response: the server returned neither data nor errors")]
109 EmptyResponse,
110 #[error("empty response field: {0}")]
112 EmptyResponseField(&'static str),
113 #[error("unknown {0} variant")]
116 UnknownVariant(&'static str),
117 #[error("deserialization error: {0}")]
119 Deserialization(#[source] BoxError),
120 #[error("parse error: {0}")]
122 Parse(#[source] BoxError),
123 #[error("invalid name `{name}`: {source}")]
125 InvalidName {
126 name: String,
128 #[source]
129 source: IotaNamesError,
130 },
131 #[error("type mismatch: expected `{expected}`, got `{actual}`")]
133 TypeMismatch {
134 expected: TypeTag,
136 actual: TypeTag,
138 },
139 #[error("invalid argument: {0}")]
142 InvalidArgument(&'static str),
143 #[error("unsupported RPC scheme `{0}` for subscriptions, expected http, https, ws or wss")]
145 UnsupportedSubscriptionScheme(String),
146 #[error(
148 "scheme `{0}` needs TLS: enable the `tls-ring` or `tls-aws-lc` feature, or pass your own \
149 client to `new_with_reqwest_client`"
150 )]
151 TlsUnavailable(String),
152 #[error("timed out")]
154 Timeout,
155 #[error("faucet error: {0}")]
157 Faucet(#[from] FaucetError),
158 #[error("subscription error: {0}")]
160 Subscription(#[source] BoxError),
161 #[error("subscription lagged: {count} payload(s) dropped by the server")]
165 Lagged { count: u32 },
166}
167
168#[derive(Debug)]
171pub struct HttpResponse {
172 url: Url,
173 status: StatusCode,
174 body: String,
175 target_type: &'static str,
176}
177
178impl HttpResponse {
179 fn new(url: Url, status: StatusCode, body: &[u8], target_type: &'static str) -> Box<Self> {
180 Box::new(Self {
181 url,
182 status,
183 body: truncated_body(body),
184 target_type,
185 })
186 }
187
188 pub fn url(&self) -> &Url {
190 &self.url
191 }
192
193 pub fn status(&self) -> StatusCode {
195 self.status
196 }
197
198 pub fn body(&self) -> &str {
200 &self.body
201 }
202
203 pub fn target_type(&self) -> &'static str {
206 self.target_type
207 }
208}
209
210impl GraphQLError {
211 pub(crate) fn http(
214 url: Url,
215 status: StatusCode,
216 body: &[u8],
217 target_type: &'static str,
218 ) -> Self {
219 Self::Http {
220 response: HttpResponse::new(url, status, body, target_type),
221 }
222 }
223
224 pub(crate) fn json(
227 url: Url,
228 status: StatusCode,
229 body: &[u8],
230 target_type: &'static str,
231 source: serde_json::Error,
232 ) -> Self {
233 Self::Json {
234 response: HttpResponse::new(url, status, body, target_type),
235 source,
236 }
237 }
238
239 pub(crate) fn deserialization<E: Into<BoxError>>(error: E) -> Self {
241 Self::Deserialization(error.into())
242 }
243
244 pub(crate) fn subscription<E: Into<BoxError>>(error: E) -> Self {
246 Self::Subscription(error.into())
247 }
248}
249
250impl From<iota_types::BcsError> for GraphQLError {
251 fn from(error: iota_types::BcsError) -> Self {
252 Self::Deserialization(error.into())
253 }
254}
255
256impl From<url::ParseError> for GraphQLError {
257 fn from(error: url::ParseError) -> Self {
258 Self::Parse(error.into())
259 }
260}
261
262impl From<ParseIntError> for GraphQLError {
263 fn from(error: ParseIntError) -> Self {
264 Self::Parse(error.into())
265 }
266}
267
268impl From<AddressParseError> for GraphQLError {
269 fn from(error: AddressParseError) -> Self {
270 Self::Parse(error.into())
271 }
272}
273
274impl From<DigestParseError> for GraphQLError {
275 fn from(error: DigestParseError) -> Self {
276 Self::Parse(error.into())
277 }
278}
279
280impl From<TryFromIntError> for GraphQLError {
281 fn from(error: TryFromIntError) -> Self {
282 Self::Parse(error.into())
283 }
284}
285
286impl From<TypeParseError> for GraphQLError {
287 fn from(error: TypeParseError) -> Self {
288 Self::Parse(error.into())
289 }
290}
291
292impl From<FromUtf8Error> for GraphQLError {
293 fn from(error: FromUtf8Error) -> Self {
294 Self::Parse(error.into())
295 }
296}
297
298#[cfg(feature = "move-types")]
299impl From<iota_move_types::FromObjectError> for GraphQLError {
300 fn from(error: iota_move_types::FromObjectError) -> Self {
301 Self::Deserialization(error.into())
302 }
303}
304
305#[cfg(all(test, not(target_arch = "wasm32")))]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn http_error_surfaces_status_body_and_decode_target() {
311 let url = Url::parse("https://graphql.devnet.iota.cafe").unwrap();
312 let error = GraphQLError::http(
313 url,
314 StatusCode::TOO_MANY_REQUESTS,
315 b"Too Many Requests",
316 "my_crate::MyResponse",
317 );
318 assert_eq!(
319 error.to_string(),
320 "GraphQL request to https://graphql.devnet.iota.cafe/ failed with HTTP 429 Too Many \
321 Requests while decoding `my_crate::MyResponse`, body=\"Too Many Requests\""
322 );
323 assert!(
325 matches!(error, GraphQLError::Http { response } if response.status == StatusCode::TOO_MANY_REQUESTS)
326 );
327 }
328
329 #[test]
330 fn json_error_surfaces_status_body_and_source() {
331 use std::error::Error as _;
332
333 let url = Url::parse("https://graphql.devnet.iota.cafe").unwrap();
334 let serde_error = serde_json::from_slice::<serde_json::Value>(b"not json").unwrap_err();
335 let error = GraphQLError::json(
336 url,
337 StatusCode::OK,
338 b"not json",
339 "my_crate::MyResponse",
340 serde_error,
341 );
342 assert_eq!(
344 error.to_string(),
345 "GraphQL request to https://graphql.devnet.iota.cafe/ returned HTTP 200 OK but the \
346 body could not be parsed as JSON while decoding `my_crate::MyResponse` (body=\"not \
347 json\"): expected ident at line 1 column 2"
348 );
349 assert_eq!(
350 error.source().expect("expected a cause").to_string(),
351 "expected ident at line 1 column 2"
352 );
353 }
354
355 #[test]
361 fn chain_formatters_repeat_the_wrapped_message() {
362 use std::error::Error as _;
363
364 let error = GraphQLError::from(iota_types::BcsError::new(
365 bcs::from_bytes::<u64>(&[]).unwrap_err(),
366 ));
367
368 assert_eq!(
369 error.to_string(),
370 "deserialization error: unexpected end of input"
371 );
372
373 let mut chain = vec![error.to_string()];
374 let mut next = error.source();
375 while let Some(error) = next {
376 chain.push(error.to_string());
377 next = error.source();
378 }
379
380 assert_eq!(
381 chain,
382 [
383 "deserialization error: unexpected end of input",
384 "unexpected end of input",
385 ]
386 );
387 }
388
389 #[test]
390 fn body_is_truncated() {
391 let body = vec![b'a'; MAX_ERROR_BODY_BYTES + 100];
392 let rendered = truncated_body(&body);
393 assert!(rendered.contains("… (truncated)"));
394 assert_eq!(rendered.len(), MAX_ERROR_BODY_BYTES + "… (truncated)".len());
395 }
396}