Skip to main content

typedb_driver/common/
error.rs

1/*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements.  See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership.  The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License.  You may obtain a copy of the License at
9 *
10 *   http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied.  See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20use std::{error::Error as StdError, fmt, num::ParseIntError, time::Duration};
21
22use tonic::{Code, Status};
23use tonic_types::{ErrorInfo, StatusExt};
24
25use super::RequestID;
26use crate::common::address::{Address, Addresses};
27
28macro_rules! error_messages {
29    {
30        $name:ident code: $code_pfx:literal, type: $message_pfx:literal,
31        $($error_name:ident $({ $($field:ident : $inner:ty),+ $(,)? })? = $code:literal: $body:literal),+ $(,)?
32    } => {
33        #[derive(Clone, Eq, PartialEq)]
34        pub enum $name {$(
35            $error_name$( { $($field: $inner),+ })?,
36        )*}
37
38        impl $name {
39            pub const PREFIX: &'static str = $code_pfx;
40
41            pub const fn code(&self) -> usize {
42                match self {$(
43                    Self::$error_name $({ $($field: _),+ })? => $code,
44                )*}
45            }
46
47            pub fn format_code(&self) -> String {
48                format!(concat!("[", $code_pfx, "{}{}]"), self.padding(), self.code())
49            }
50
51            pub fn message(&self) -> String {
52                match self {$(
53                    Self::$error_name $({$($field),+})? => format!($body $($(, $field = $field)+)?),
54                )*}
55            }
56
57            const fn max_code() -> usize {
58                let mut max = usize::MIN;
59                $(max = if $code > max { $code } else { max };)*
60                max
61            }
62
63            const fn num_digits(x: usize) -> usize {
64                if (x < 10) { 1 } else { 1 + Self::num_digits(x/10) }
65            }
66
67            const fn padding(&self) -> &'static str {
68                match Self::num_digits(Self::max_code()) - Self::num_digits(self.code()) {
69                    0 => "",
70                    1 => "0",
71                    2 => "00",
72                    3 => "000",
73                    _ => unreachable!(),
74                }
75            }
76
77            const fn name(&self) -> &'static str {
78                match self {$(
79                    Self::$error_name $({ $($field: _),+ })? => concat!(stringify!($name), "::", stringify!($error_name)),
80                )*}
81            }
82        }
83
84        impl fmt::Display for $name {
85            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86                write!(
87                    f,
88                    concat!("[", $code_pfx, "{}{}] ", $message_pfx, ": {}"),
89                    self.padding(),
90                    self.code(),
91                    self.message()
92                )
93            }
94        }
95
96        impl fmt::Debug for $name {
97            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98                let mut debug_struct = f.debug_struct(self.name());
99                debug_struct.field("message", &format!("{}", self));
100                $(
101                    $(
102                        if let Self::$error_name { $($field),+ } = &self {
103                            $(debug_struct.field(stringify!($field), &$field);)+
104                        }
105                    )?
106                )*
107                debug_struct.finish()
108            }
109        }
110
111        impl std::error::Error for $name {
112            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
113                None
114            }
115        }
116    };
117}
118
119error_messages! { AnalyzeError
120    code: "ANZ", type: "Analyze Error",
121    MissingResponseField { field: &'static str } =
122        1: "Missing field in message received from server: '{field}'. This is either a version compatibility issue or a bug.",
123    UnknownEnumValue { enum_name: &'static str, value: i32 } =
124        2: "Value '{value}' is out of bounds for enum '{enum_name}'. This is either a version compatibility issue or a bug.",
125}
126
127error_messages! { ConnectionError
128    code: "CXN", type: "Connection Error",
129    RPCMethodUnavailable { message: String } =
130        1: "The server does not support this method, please check the driver-server compatibility:\n'{message}'.",
131    ServerConnectionFailed { configured_addresses: Addresses, accessed_addresses: Addresses, details: String } =
132        2: "Unable to connect to TypeDB server(s).\nInitially configured addresses: {configured_addresses}.\nTried accessing addresses: {accessed_addresses}. Details: {details}",
133    ServerConnectionFailedWithError { error: String } =
134        3: "Unable to connect to TypeDB server(s), received errors: \n{error}",
135    ServerConnectionFailedNetworking { error: String } =
136        4: "Unable to connect to TypeDB server(s), received network or protocol error: \n{error}",
137    ServerConnectionIsClosed =
138        5: "The connection has been closed and no further operation is allowed.",
139    ServerConnectionIsClosedUnexpectedly =
140        6: "The connection has been closed unexpectedly and no further operation is allowed.",
141    TransactionIsClosed =
142        7: "The transaction is closed and no further operation is allowed.",
143    TransactionIsClosedWithErrors { errors: String } =
144        8: "The transaction is closed because of the error(s):\n{errors}",
145    MissingResponseField { field: &'static str } =
146        9: "Missing field in message received from server: '{field}'. This is either a version compatibility issue or a bug.",
147    UnknownRequestId { request_id: RequestID } =
148        10: "Received a response with unknown request id '{request_id}'",
149    UnexpectedResponse { response: String } =
150        11: "Received unexpected response from server: '{response}'. This is either a version compatibility issue or a bug.",
151    QueryStreamNoResponse =
152        12: "Didn't receive any server responses for the query.",
153    UnexpectedQueryType { query_type: i32 } =
154        13: "Unexpected query type in message received from server: {query_type}. This is either a version compatibility issue or a bug.",
155    ClusterServerNotPrimary { hint_address: Option<Address> } =
156        14: "The server is not primary (primary hint: {hint_address:?}).",
157    UnknownDirectServerRouting { address: Address, known_addresses: Addresses } =
158        15: "Could not execute operation against '{address}' since it's not in the list of known servers: {known_addresses}.",
159    TokenCredentialInvalid =
160        16: "Invalid credential supplied.",
161    EncryptionSettingsMismatch =
162        17: "Unable to connect to TypeDB: possible encryption settings mismatch.",
163    SslCertificateNotValidated =
164        18: "SSL handshake with TypeDB failed: the server's identity could not be verified. Possible CA mismatch.",
165    BrokenPipe =
166        19: "Stream closed because of a broken pipe. This could happen if you are attempting to connect to an unencrypted TypeDB server using a TLS-enabled credentials.",
167    ConnectionRefusedNetworking =
168        20: "Connection refused. Please check the server is running and the address is accessible. Encrypted TypeDB endpoints may also have misconfigured SSL certificates.",
169    MissingPort { address: String } =
170        21: "Invalid URL '{address}': missing port.",
171    UnexpectedServerReplicationRole { replication_role: i32 } =
172        22: "Unexpected replication role in message received from server: {replication_role}. This is either a version compatibility issue or a bug.",
173    ValueTimeZoneNameNotRecognised { time_zone: String } =
174        23: "Time zone provided by the server has name '{time_zone}', which is not an officially recognized timezone.",
175    ValueTimeZoneOffsetNotRecognised { offset: i32 } =
176        24: "Time zone provided by the server has numerical offset '{offset}', which is not recognised as a valid value for offset in seconds.",
177    ValueStructNotImplemented =
178        25: "Struct valued responses are not yet supported by the driver.",
179    ListsNotImplemented =
180        26: "Lists are not yet supported by the driver.",
181    UnexpectedKind { kind: i32 } =
182        27: "Unexpected kind in message received from server: {kind}. This is either a version compatibility issue or a bug.",
183    UnexpectedConnectionClose =
184        28: "Connection closed unexpectedly.",
185    DatabaseImportChannelIsClosed =
186        29: "The database import channel is closed and no further operation is allowed.",
187    DatabaseImportStreamUnexpectedResponse =
188        30: "The database import stream received an unexpected response in the process. It is either a version compatibility issue or a bug.",
189    DatabaseExportChannelIsClosed =
190        31: "The database export channel is closed and no further operation is allowed.",
191    DatabaseExportStreamNoResponse =
192        32: "Didn't receive any server responses for the database export command.",
193    AbsentTlsConfigForTlsConnection =
194        33: "Could not establish a TLS connection without a TLS config specified. Please verify your driver options.",
195    ServerIsNotInitialised =
196        34: "Server is not yet initialized.",
197    AnalyzeNoResponse =
198        35: "Didn't receive any server responses for the analyze request.",
199    NoPrimaryServer =
200        37: "Could not find a primary server.",
201    SchemeTlsSettingsMismatch { scheme: http::uri::Scheme, is_tls_enabled: bool } =
202        38: "Scheme {scheme} is not compatible with tls setting `enabled: {is_tls_enabled}`",
203    RequestTimeout { timeout: String } =
204        39: "Request timed out after {timeout}. The server may be unresponsive.",
205}
206
207impl ConnectionError {
208    pub fn request_timeout(timeout: Duration) -> Self {
209        let (value, decimal, unit) = if timeout.as_secs() > 0 {
210            let decimal = timeout.subsec_millis();
211            (timeout.as_secs(), (decimal > 0).then_some(decimal), "seconds")
212        } else if timeout.subsec_millis() > 0 {
213            let decimal = timeout.subsec_micros() % 1000;
214            (timeout.subsec_millis() as u64, (decimal > 0).then_some(decimal), "milliseconds")
215        } else if timeout.subsec_micros() > 0 {
216            let decimal = timeout.subsec_nanos() % 1000;
217            (timeout.subsec_micros() as u64, (decimal > 0).then_some(decimal), "microseconds")
218        } else {
219            (timeout.subsec_nanos() as u64, None, "nanoseconds")
220        };
221
222        let timeout_str = match decimal {
223            Some(d) => format!("{value}.{d:03} {unit}"),
224            None => format!("{value} {unit}"),
225        };
226        ConnectionError::RequestTimeout { timeout: timeout_str }
227    }
228}
229
230error_messages! { ConceptError
231    code: "CPT", type: "Concept Error",
232    UnavailableRowVariable { variable: String } =
233        1: "Cannot get concept from a concept row by variable '{variable}'.",
234    UnavailableRowIndex { index: usize } =
235        2: "Cannot get concept from a concept row by index '{index}'.",
236    ErrorParsingDecimal { unparsed: String, reason: String } =
237        3: "Could not parse the value '{unparsed}' as decimal: {reason}.",
238}
239
240error_messages! { MigrationError
241    code: "MGT", type: "Migration Error",
242    CannotOpenImportFile { path: String, reason: String } =
243        1: "Cannot open import file '{path}': {reason}",
244    CannotCreateExportFile { path: String, reason: String } =
245        2: "Cannot create export file '{path}': {reason}",
246    CannotExportToTheSameFile =
247        3: "Cannot export both schema and data to the same file.",
248    CannotDecodeImportedConcept =
249        4: "Cannot decode a concept from the provided import file. Make sure to pass a correct database file produced by a TypeDB export operation.",
250    CannotDecodeImportedConceptLength =
251        5: "Cannot decode a concept length from the provided import file. Make sure to pass a correct database file produced by a TypeDB export operation.",
252    CannotEncodeExportedConcept =
253        6: "Cannot encode a concept for export. It's either a version compatibility error or a bug."
254}
255
256error_messages! { InternalError
257    code: "INT", type: "Internal Error",
258    RecvError =
259        1: "Channel is closed.",
260    SendError =
261        2: "Unable to send response over callback channel (receiver dropped).",
262    UnexpectedRequestType { request_type: String } =
263        3: "Unexpected request type for remote procedure call: {request_type}. This is either a version compatibility issue or a bug.",
264    UnexpectedResponseType { response_type: String } =
265        4: "Unexpected response type for remote procedure call: {response_type}. This is either a version compatibility issue or a bug.",
266    Unimplemented { details: String } =
267        5: "Unimplemented feature: {details}.",
268}
269
270error_messages! { QueryError
271    code: "QRY", type: "Internal Error",
272    GivenRowIndexOutOfBounds { index: usize, width: usize } =
273        1: "Attempted to set a row entry at index: {index}. Row width is {width}.",
274    GivenRowUnknownVariable { variable: String } =
275        2: "Attempted to set a row entry for a variable {variable} which was not in the header.",
276    GivenRowsSizeMismatch { actual: usize, expected: usize } =
277        3: "Cannot push a row of width: {actual} . Expected width: {expected}",
278    GivenRowsReceivedType { variable_or_index: String } =
279        4: "A type was passed as given row entry for '{variable_or_index}'. Only instances and values are allowed.",
280    InvalidTypeToGivenRow =
281        5: "Types cannot be converted to GivenRowEntry. Only instances and values can.",
282}
283
284#[derive(Clone, PartialEq, Eq)]
285pub struct ServerError {
286    error_code: String,
287    error_domain: String,
288    message: String,
289    stack_trace: Vec<String>,
290}
291
292impl ServerError {
293    pub(crate) fn new(error_code: String, error_domain: String, message: String, stack_trace: Vec<String>) -> Self {
294        Self { error_code, error_domain, message, stack_trace }
295    }
296
297    pub(crate) fn format_code(&self) -> &str {
298        &self.error_code
299    }
300
301    pub(crate) fn message(&self) -> String {
302        self.to_string()
303    }
304}
305
306impl fmt::Display for ServerError {
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        if self.stack_trace.is_empty() {
309            write!(f, "[{}] {}. {}", self.error_code, self.error_domain, self.message)
310        } else {
311            write!(f, "\n{}", self.stack_trace.join("\nCaused: "))
312        }
313    }
314}
315
316impl fmt::Debug for ServerError {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        fmt::Display::fmt(self, f)
319    }
320}
321
322/// Represents errors encountered during operation.
323#[derive(Clone, Debug, PartialEq, Eq)]
324pub enum Error {
325    Analyze(AnalyzeError),
326    Connection(ConnectionError),
327    Concept(ConceptError),
328    Migration(MigrationError),
329    Internal(InternalError),
330    Query(QueryError),
331    Server(ServerError),
332    Other(String),
333    FFI(String),
334}
335
336impl Error {
337    pub fn code(&self) -> String {
338        match self {
339            Self::Analyze(error) => error.format_code(),
340            Self::Connection(error) => error.format_code(),
341            Self::Concept(error) => error.format_code(),
342            Self::Migration(error) => error.format_code(),
343            Self::Internal(error) => error.format_code(),
344            Self::Query(error) => error.format_code(),
345            Self::Server(error) => error.format_code().to_owned(),
346            Self::FFI(_error) => String::new(),
347            Self::Other(_error) => String::new(),
348        }
349    }
350
351    pub fn message(&self) -> String {
352        match self {
353            Self::Analyze(error) => error.message(),
354            Self::Connection(error) => error.message(),
355            Self::Concept(error) => error.message(),
356            Self::Migration(error) => error.message(),
357            Self::Internal(error) => error.message(),
358            Self::Query(error) => error.message(),
359            Self::Server(error) => error.message(),
360            Self::FFI(error) => error.clone(),
361            Self::Other(error) => error.clone(),
362        }
363    }
364
365    fn try_extracting_connection_error_from_info(error_info: &ErrorInfo) -> Option<ConnectionError> {
366        match error_info.reason.as_str() {
367            "AUT1" | "AUT2" | "AUT3" => Some(ConnectionError::TokenCredentialInvalid {}),
368            "SRV14" | "RFT1" | "CSV7" => Some(ConnectionError::ServerIsNotInitialised {}),
369            "CSV8" => Some(ConnectionError::ClusterServerNotPrimary { hint_address: None }),
370            "CSV9" => Some(ConnectionError::ClusterServerNotPrimary {
371                hint_address: error_info.metadata.get("redirect_address").and_then(|addr| addr.parse::<Address>().ok()),
372            }),
373            _ => None,
374        }
375    }
376
377    fn try_extracting_connection_error_message(message: &str) -> Option<ConnectionError> {
378        if is_rst_stream(message) || is_tcp_connect_error(message) || is_connection_error(message) {
379            Some(ConnectionError::ServerConnectionFailedNetworking { error: message.to_string() })
380        } else if is_reading_body_from_connection_error(message) {
381            Some(ConnectionError::ServerConnectionIsClosedUnexpectedly)
382        } else {
383            None
384        }
385    }
386
387    fn parse_unavailable(status_message: &str) -> Error {
388        if status_message == "broken pipe" {
389            Error::Connection(ConnectionError::BrokenPipe)
390        } else if status_message.contains("received corrupt message") {
391            Error::Connection(ConnectionError::EncryptionSettingsMismatch)
392        } else if status_message.contains("UnknownIssuer") {
393            Error::Connection(ConnectionError::SslCertificateNotValidated)
394        } else if status_message.contains("Connection refused") {
395            Error::Connection(ConnectionError::ConnectionRefusedNetworking)
396        } else {
397            Error::Connection(ConnectionError::ServerConnectionFailedNetworking { error: status_message.to_owned() })
398        }
399    }
400}
401
402impl fmt::Display for Error {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        match self {
405            Self::Analyze(error) => write!(f, "{error}"),
406            Self::Connection(error) => write!(f, "{error}"),
407            Self::Concept(error) => write!(f, "{error}"),
408            Self::Migration(error) => write!(f, "{error}"),
409            Self::Internal(error) => write!(f, "{error}"),
410            Self::Query(error) => write!(f, "{error}"),
411            Self::Server(error) => write!(f, "{error}"),
412            Self::FFI(message) => write!(f, "{message}"),
413            Self::Other(message) => write!(f, "{message}"),
414        }
415    }
416}
417
418impl StdError for Error {
419    fn source(&self) -> Option<&(dyn StdError + 'static)> {
420        match self {
421            Self::Analyze(error) => Some(error),
422            Self::Connection(error) => Some(error),
423            Self::Concept(error) => Some(error),
424            Self::Migration(error) => Some(error),
425            Self::Internal(error) => Some(error),
426            Self::Query(_) => None,
427            Self::Server(_) => None,
428            Self::FFI(_) => None,
429            Self::Other(_) => None,
430        }
431    }
432}
433
434impl From<AnalyzeError> for Error {
435    fn from(error: AnalyzeError) -> Self {
436        Self::Analyze(error)
437    }
438}
439
440impl From<ConnectionError> for Error {
441    fn from(error: ConnectionError) -> Self {
442        Self::Connection(error)
443    }
444}
445
446impl From<ConceptError> for Error {
447    fn from(error: ConceptError) -> Self {
448        Self::Concept(error)
449    }
450}
451
452impl From<MigrationError> for Error {
453    fn from(error: MigrationError) -> Self {
454        Self::Migration(error)
455    }
456}
457
458impl From<InternalError> for Error {
459    fn from(error: InternalError) -> Self {
460        Self::Internal(error)
461    }
462}
463
464impl From<ServerError> for Error {
465    fn from(error: ServerError) -> Self {
466        Self::Server(error)
467    }
468}
469
470impl From<Status> for Error {
471    fn from(status: Status) -> Self {
472        if let Ok(details) = status.check_error_details() {
473            if let Some(bad_request) = details.bad_request() {
474                return Self::Connection(ConnectionError::ServerConnectionFailedWithError {
475                    error: format!("{:?}", bad_request),
476                });
477            }
478
479            let message = concat_source_messages(&status);
480            if let Some(connection_error) = Self::try_extracting_connection_error_message(&message) {
481                return Self::Connection(connection_error);
482            }
483
484            if let Some(error_info) = details.error_info() {
485                if let Some(connection_error) = Self::try_extracting_connection_error_from_info(error_info) {
486                    Self::Connection(connection_error)
487                } else {
488                    let code = error_info.reason.clone();
489                    let domain = error_info.domain.clone();
490                    let stack_trace =
491                        details.debug_info().map(|debug_info| debug_info.stack_entries.clone()).unwrap_or_default();
492                    Self::Server(ServerError::new(code, domain, status.message().to_owned(), stack_trace))
493                }
494            } else {
495                Self::Other(message)
496            }
497        } else {
498            match status.code() {
499                Code::Unavailable => Self::parse_unavailable(status.message()),
500                Code::Unknown | Code::FailedPrecondition | Code::AlreadyExists => {
501                    Self::Connection(ConnectionError::ServerConnectionFailedNetworking {
502                        error: status.message().to_owned(),
503                    })
504                }
505                Code::Unimplemented => {
506                    Self::Connection(ConnectionError::RPCMethodUnavailable { message: status.message().to_owned() })
507                }
508                _ => {
509                    let message = concat_source_messages(&status);
510                    Self::try_extracting_connection_error_message(&message)
511                        .map(|error| Self::Connection(error))
512                        .unwrap_or_else(|| Self::Other(message))
513                }
514            }
515        }
516    }
517}
518
519fn is_rst_stream(message: &str) -> bool {
520    // "Received Rst Stream" occurs if the server is in the process of shutting down.
521    message.contains("Received Rst Stream")
522}
523
524fn is_reading_body_from_connection_error(message: &str) -> bool {
525    // This error can be returned when the server crashes
526    message.contains("error reading a body from connection")
527}
528
529fn is_tcp_connect_error(message: &str) -> bool {
530    // No TCP connection
531    message.contains("tcp connect error")
532}
533
534fn is_connection_error(message: &str) -> bool {
535    // Transport-level connection errors
536    message.contains("connection error")
537}
538
539fn concat_source_messages(status: &Status) -> String {
540    let mut errors = String::new();
541    errors.push_str(status.message());
542    let mut err: Option<&dyn std::error::Error> = status.source();
543    while let Some(e) = err {
544        errors.push_str(": ");
545        errors.push_str(e.to_string().as_str());
546        err = e.source();
547    }
548    errors
549}
550
551impl From<http::uri::InvalidUri> for Error {
552    fn from(err: http::uri::InvalidUri) -> Self {
553        Self::Other(err.to_string())
554    }
555}
556
557impl From<tonic::transport::Error> for Error {
558    fn from(err: tonic::transport::Error) -> Self {
559        Self::Other(err.to_string())
560    }
561}
562
563impl<T> From<tokio::sync::mpsc::error::SendError<T>> for Error {
564    fn from(_err: tokio::sync::mpsc::error::SendError<T>) -> Self {
565        Self::Internal(InternalError::SendError)
566    }
567}
568
569impl From<tokio::sync::oneshot::error::RecvError> for Error {
570    fn from(_err: tokio::sync::oneshot::error::RecvError) -> Self {
571        Self::Internal(InternalError::RecvError)
572    }
573}
574
575impl From<crossbeam::channel::RecvError> for Error {
576    fn from(_err: crossbeam::channel::RecvError) -> Self {
577        Self::Internal(InternalError::RecvError)
578    }
579}
580
581impl<T> From<crossbeam::channel::SendError<T>> for Error {
582    fn from(_err: crossbeam::channel::SendError<T>) -> Self {
583        Self::Internal(InternalError::SendError)
584    }
585}
586
587impl From<String> for Error {
588    fn from(err: String) -> Self {
589        Self::Other(err)
590    }
591}
592
593impl From<std::io::Error> for Error {
594    fn from(err: std::io::Error) -> Self {
595        Self::Other(err.to_string())
596    }
597}