connect_rpc/response/
error.rs

1use http::{header, HeaderMap, HeaderValue};
2
3use crate::{common::base64_decode, metadata::Metadata, Error};
4
5const ERROR_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("application/json");
6
7/// A Connect error.
8#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
9pub struct ConnectError {
10    #[serde(default, deserialize_with = "deserialize_error_code")]
11    code: Option<ConnectCode>,
12    #[serde(default, skip_serializing_if = "String::is_empty")]
13    pub message: String,
14    #[serde(default, skip_serializing_if = "Vec::is_empty")]
15    pub details: Vec<ConnectErrorDetail>,
16    #[serde(skip)]
17    headers: HeaderMap,
18}
19
20impl ConnectError {
21    pub fn new(code: ConnectCode, message: impl std::fmt::Display) -> Self {
22        Self {
23            code: Some(code),
24            message: message.to_string(),
25            details: Default::default(),
26            headers: Default::default(),
27        }
28    }
29
30    pub fn code(&self) -> ConnectCode {
31        self.code.unwrap_or(ConnectCode::Unknown)
32    }
33
34    pub fn metadata(&self) -> &impl Metadata {
35        &self.headers
36    }
37}
38
39impl std::fmt::Display for ConnectError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.write_str(serde_json::to_value(self.code()).unwrap().as_str().unwrap())?;
42        if !self.message.is_empty() {
43            write!(f, ": {}", self.message)?;
44        }
45        Ok(())
46    }
47}
48
49impl<T: AsRef<[u8]>> From<http::Response<T>> for ConnectError {
50    fn from(resp: http::Response<T>) -> Self {
51        let (parts, body) = resp.into_parts();
52        let error = if parts.headers.get(header::CONTENT_TYPE) == Some(&ERROR_CONTENT_TYPE) {
53            match serde_json::from_slice::<ConnectError>(body.as_ref()) {
54                Ok(mut error) => {
55                    error.code.get_or_insert_with(|| parts.status.into());
56                    Some(error)
57                }
58                Err(err) => {
59                    tracing::debug!(?err, "Failed to decode error JSON");
60                    None
61                }
62            }
63        } else {
64            None
65        };
66        let mut error = error.unwrap_or_else(|| Self::new(parts.status.into(), "request invalid"));
67        error.headers = parts.headers;
68        error
69    }
70}
71
72impl From<Error> for ConnectError {
73    fn from(err: Error) -> Self {
74        let code = match err {
75            Error::ConnectError(connect_error) => return connect_error,
76            Error::InvalidResponse(_)
77            | Error::UnacceptableEncoding(_)
78            | Error::UnexpectedMessageCodec(_) => ConnectCode::Internal,
79            _ => ConnectCode::Unknown,
80        };
81        let message = match &err {
82            Error::UnacceptableEncoding(_) | Error::UnexpectedMessageCodec(_) => err.to_string(),
83            _ => "".into(),
84        };
85        Self::new(code, message)
86    }
87}
88
89fn deserialize_error_code<'de, D: serde::Deserializer<'de>>(
90    deserializer: D,
91) -> Result<Option<ConnectCode>, D::Error> {
92    use serde::Deserialize;
93    Option::<ConnectCode>::deserialize(deserializer).or(Ok(None))
94}
95
96/// ConnectCode represents categories of errors as codes.
97#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum ConnectCode {
100    /// The operation completed successfully.
101    Ok,
102    /// The operation was cancelled.
103    Canceled,
104    /// Unknown error.
105    Unknown,
106    /// Client specified an invalid argument.
107    InvalidArgument,
108    /// Deadline expired before operation could complete.
109    DeadlineExceeded,
110    /// Some requested entity was not found.
111    NotFound,
112    /// Some entity that we attempted to create already exists.
113    AlreadyExists,
114    /// The caller does not have permission to execute the specified operation.
115    PermissionDenied,
116    /// Some resource has been exhausted.
117    ResourceExhausted,
118    /// The system is not in a state required for the operation's execution.
119    FailedPrecondition,
120    /// The operation was aborted.
121    Aborted,
122    /// Operation was attempted past the valid range.
123    OutOfRange,
124    /// Operation is not implemented or not supported.
125    Unimplemented,
126    /// Internal error.
127    Internal,
128    /// The service is currently unavailable.
129    Unavailable,
130    /// Unrecoverable data loss or corruption.
131    DataLoss,
132    /// The request does not have valid authentication credentials
133    Unauthenticated,
134}
135
136// https://connectrpc.com/docs/protocol/#http-to-error-code
137impl From<http::StatusCode> for ConnectCode {
138    fn from(code: http::StatusCode) -> Self {
139        use http::StatusCode;
140        match code {
141            StatusCode::BAD_REQUEST => Self::Internal,
142            StatusCode::UNAUTHORIZED => Self::Unauthenticated,
143            StatusCode::FORBIDDEN => Self::PermissionDenied,
144            StatusCode::NOT_FOUND => Self::Unimplemented,
145            StatusCode::NOT_IMPLEMENTED => Self::Unimplemented,
146            StatusCode::TOO_MANY_REQUESTS
147            | StatusCode::BAD_GATEWAY
148            | StatusCode::SERVICE_UNAVAILABLE
149            | StatusCode::GATEWAY_TIMEOUT => Self::Unavailable,
150            _ => Self::Unknown,
151        }
152    }
153}
154
155/// Connect error detail.
156#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
157pub struct ConnectErrorDetail {
158    #[serde(rename = "type")]
159    pub proto_type: String,
160    #[serde(rename = "value")]
161    pub value_base64: String,
162}
163
164impl ConnectErrorDetail {
165    pub fn type_url(&self) -> String {
166        format!("type.googleapis.com/{}", self.proto_type)
167    }
168
169    pub fn value(&self) -> Result<Vec<u8>, Error> {
170        base64_decode(&self.value_base64)
171    }
172}