Skip to main content

autoendpoint/routers/fcm/
error.rs

1use crate::error::ApiErrorKind;
2use crate::routers::RouterError;
3
4use autopush_common::errors::ReportableError;
5use reqwest::StatusCode;
6
7/// Errors that may occur in the Firebase Cloud Messaging router
8#[derive(thiserror::Error, Debug)]
9pub enum FcmError {
10    #[error("Failed to decode the credential settings")]
11    CredentialDecode(#[from] serde_json::Error),
12
13    #[error("Error while building the OAuth client")]
14    OAuthClientBuild(#[source] std::io::Error),
15
16    #[error("Error while retrieving an OAuth token")]
17    OAuthToken(#[from] yup_oauth2::Error),
18
19    #[error("Unable to deserialize FCM response")]
20    DeserializeResponse(#[source] reqwest::Error),
21
22    #[error("Invalid JSON response from FCM")]
23    InvalidResponse(#[source] serde_json::Error, String, StatusCode),
24
25    #[error("Empty response from FCM")]
26    EmptyResponse(StatusCode),
27
28    #[error("No OAuth token was present")]
29    NoOAuthToken,
30
31    #[error("No registration token found for user")]
32    NoRegistrationToken,
33
34    #[error("No app ID found for user")]
35    NoAppId,
36
37    #[error("User has invalid app ID {0}")]
38    InvalidAppId(String),
39
40    #[error("Upstream error, {error_code}: {message}")]
41    Upstream {
42        error_code: String,
43        message: String,
44        /// Sourced from upstream `Retry-After` if provided (otherise default)
45        retry_after: Option<u64>,
46    },
47}
48
49impl FcmError {
50    /// Get the associated HTTP status code
51    pub fn status(&self) -> StatusCode {
52        match self {
53            FcmError::InvalidAppId(_) => StatusCode::BAD_REQUEST,
54
55            FcmError::NoRegistrationToken | FcmError::NoAppId => StatusCode::GONE,
56
57            FcmError::CredentialDecode(_)
58            | FcmError::OAuthClientBuild(_)
59            | FcmError::OAuthToken(_)
60            | FcmError::NoOAuthToken => StatusCode::INTERNAL_SERVER_ERROR,
61
62            // FCM is rate-limiting us
63            FcmError::Upstream { error_code, .. } if error_code == "RESOURCE_EXHAUSTED" => {
64                StatusCode::TOO_MANY_REQUESTS
65            }
66            // FCM is transiently unable to accept the message
67            FcmError::Upstream { error_code, .. } if error_code == "UNAVAILABLE" => {
68                StatusCode::SERVICE_UNAVAILABLE
69            }
70
71            FcmError::DeserializeResponse(_)
72            | FcmError::EmptyResponse(_)
73            | FcmError::InvalidResponse(_, _, _)
74            | FcmError::Upstream { .. } => StatusCode::BAD_GATEWAY,
75        }
76    }
77
78    /// Get the associated error number
79    pub fn errno(&self) -> Option<usize> {
80        match self {
81            FcmError::NoRegistrationToken | FcmError::NoAppId | FcmError::InvalidAppId(_) => {
82                Some(106)
83            }
84
85            FcmError::Upstream { error_code, .. }
86                if error_code == "RESOURCE_EXHAUSTED" || error_code == "UNAVAILABLE" =>
87            {
88                Some(201)
89            }
90
91            _ => None,
92        }
93    }
94
95    /// The upstream-supplied `Retry-After`, in seconds, when the bridge sent one.
96    pub fn retry_after(&self) -> Option<u64> {
97        match self {
98            FcmError::Upstream { retry_after, .. } => *retry_after,
99            _ => None,
100        }
101    }
102}
103
104impl From<FcmError> for ApiErrorKind {
105    fn from(e: FcmError) -> Self {
106        ApiErrorKind::Router(RouterError::Fcm(e))
107    }
108}
109
110impl ReportableError for FcmError {
111    fn is_sentry_event(&self) -> bool {
112        matches!(&self, FcmError::InvalidAppId(_) | FcmError::NoAppId)
113    }
114
115    fn metric_label(&self) -> Option<&'static str> {
116        match &self {
117            FcmError::InvalidAppId(_) | FcmError::NoAppId => Some("notification.bridge.error"),
118            _ => None,
119        }
120    }
121
122    fn extras(&self) -> Vec<(&str, String)> {
123        match self {
124            FcmError::InvalidAppId(appid) => {
125                vec![
126                    ("status", "bad_appid".to_owned()),
127                    ("app_id", appid.to_string()),
128                ]
129            }
130            FcmError::EmptyResponse(status) => {
131                vec![("status", status.to_string())]
132            }
133            FcmError::InvalidResponse(_, body, status) => {
134                vec![("status", status.to_string()), ("body", body.to_owned())]
135            }
136            FcmError::Upstream { error_code, .. } => {
137                vec![("status", error_code.clone())]
138            }
139            _ => vec![],
140        }
141    }
142}