Skip to main content

iota_graphql_rpc/
error.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use async_graphql::{ErrorExtensionValues, ErrorExtensions, Pos, Response, ServerError};
6use async_graphql_axum::GraphQLResponse;
7use iota_indexer::errors::IndexerError;
8use iota_indexer_streaming::error::IndexerStreamingError;
9use iota_names::error::IotaNamesError;
10
11/// Error codes for the `extensions.code` field of a GraphQL error that
12/// originates from outside GraphQL.
13/// `<https://www.apollographql.com/docs/apollo-server/data/errors/#built-in-error-codes>`
14pub(crate) mod code {
15    pub const BAD_REQUEST: &str = "BAD_REQUEST";
16    pub const BAD_USER_INPUT: &str = "BAD_USER_INPUT";
17    pub const DATA_PRUNED: &str = "DATA_PRUNED";
18    pub const INTERNAL_SERVER_ERROR: &str = "INTERNAL_SERVER_ERROR";
19    pub const REQUEST_TIMEOUT: &str = "REQUEST_TIMEOUT";
20    pub const UNKNOWN: &str = "UNKNOWN";
21}
22
23/// Create a GraphQL Response containing an Error.
24///
25/// Most errors produced by the service will automatically be wrapped in a
26/// `GraphQLResponse`, because they will originate from within the GraphQL
27/// implementation.  This function is intended for errors that originated from
28/// outside of GraphQL (such as in middleware), but that need to be ingested by
29/// GraphQL clients.
30pub(crate) fn graphql_error_response(code: &str, message: impl Into<String>) -> GraphQLResponse {
31    let error = graphql_error(code, message);
32    Response::from_errors(error.into()).into()
33}
34
35/// Create a generic GraphQL Server Error.
36///
37/// This error has no path, source, or locations, just a message and an error
38/// code.
39pub(crate) fn graphql_error(code: &str, message: impl Into<String>) -> ServerError {
40    let mut ext = ErrorExtensionValues::default();
41    ext.set("code", code);
42
43    ServerError {
44        message: message.into(),
45        source: None,
46        locations: vec![],
47        path: vec![],
48        extensions: Some(ext),
49    }
50}
51
52pub(crate) fn graphql_error_at_pos(
53    code: &str,
54    message: impl Into<String>,
55    pos: Pos,
56) -> ServerError {
57    let mut ext = ErrorExtensionValues::default();
58    ext.set("code", code);
59
60    ServerError {
61        message: message.into(),
62        source: None,
63        locations: vec![pos],
64        path: vec![],
65        extensions: Some(ext),
66    }
67}
68
69#[derive(Clone, Debug, thiserror::Error)]
70#[non_exhaustive]
71pub enum Error {
72    #[error("Unsupported protocol version requested. Min supported: {0}, max supported: {1}")]
73    ProtocolVersionUnsupported(u64, u64),
74    #[error("'first' and 'last' must not be used together")]
75    CursorNoFirstLast,
76    #[error("Connection's page size of {0} exceeds max of {1}")]
77    PageTooLarge(u64, u32),
78    // Catch-all for client-fault errors
79    #[error("{0}")]
80    Client(String),
81    #[error("Requested data has been pruned: {0}")]
82    DataPruned(String),
83    #[error("Internal error occurred while processing request: {0}")]
84    Internal(String),
85    #[error(transparent)]
86    IotaNames(#[from] IotaNamesError),
87    #[error("{0}")]
88    ServerInit(String),
89    #[error("Unsupported feature: {0}")]
90    UnsupportedFeature(String),
91}
92
93impl ErrorExtensions for Error {
94    fn extend(&self) -> async_graphql::Error {
95        async_graphql::Error::new(format!("{self}")).extend_with(|_err, e| match self {
96            Error::CursorNoFirstLast
97            | Error::PageTooLarge(_, _)
98            | Error::ProtocolVersionUnsupported(_, _)
99            | Error::Client(_) => {
100                e.set("code", code::BAD_USER_INPUT);
101            }
102            Error::DataPruned(_) => {
103                e.set("code", code::DATA_PRUNED);
104            }
105            Error::Internal(_) => {
106                e.set("code", code::INTERNAL_SERVER_ERROR);
107            }
108            Error::IotaNames(_) => {
109                e.set("code", code::BAD_REQUEST);
110            }
111            Error::ServerInit(_) => {
112                e.set("code", code::UNKNOWN);
113            }
114            Error::UnsupportedFeature(_) => {
115                e.set("code", code::BAD_REQUEST);
116            }
117        })
118    }
119}
120
121impl From<IndexerError> for Error {
122    fn from(e: IndexerError) -> Self {
123        match e {
124            IndexerError::DataPruned(msg) => Error::DataPruned(msg),
125            _ => Error::Internal(e.to_string()),
126        }
127    }
128}
129
130impl From<IndexerStreamingError> for Error {
131    fn from(e: IndexerStreamingError) -> Self {
132        Error::Internal(e.to_string())
133    }
134}