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