facebook_graph_api_object_error/
lib.rs

1//! https://developers.facebook.com/docs/graph-api/guides/error-handling
2//! https://developers.facebook.com/docs/instagram-api/reference/error-codes
3
4use serde::{Deserialize, Serialize};
5use serde_enum_str::{Deserialize_enum_str, Serialize_enum_str};
6use serde_json::{Map, Value};
7
8//
9const CODE_STATUS_CODE_AND_BODY: i32 = -2_147_483_001;
10
11//
12//
13//
14#[derive(Deserialize, Serialize, Debug, Clone)]
15pub struct Error {
16    pub message: String,
17    pub r#type: Option<ErrorType>,
18    pub code: i32,
19    pub error_subcode: Option<i32>,
20    pub error_user_title: Option<String>,
21    pub error_user_msg: Option<String>,
22    pub fbtrace_id: Option<String>,
23    /*
24    is_transient https://developers.facebook.com/docs/instagram-api/reference/error-codes
25    error_data
26    */
27    #[serde(flatten, skip_serializing_if = "Option::is_none")]
28    _extra: Option<Map<String, Value>>,
29}
30
31impl Error {
32    pub fn extra(&self) -> Option<&Map<String, Value>> {
33        self._extra.as_ref()
34    }
35
36    pub fn new_with_status_code_and_body(status_code: u16, body: &str) -> Self {
37        let mut extra = Map::new();
38        extra.insert("status_code".to_string(), Value::from(status_code));
39        extra.insert("body".to_string(), Value::from(body));
40
41        Self {
42            message: format!("status_code:{status_code} body:{body}"),
43            r#type: None,
44            code: CODE_STATUS_CODE_AND_BODY,
45            error_subcode: None,
46            error_user_title: None,
47            error_user_msg: None,
48            fbtrace_id: None,
49            _extra: Some(extra),
50        }
51    }
52
53    pub fn as_status_code_and_body(&self) -> Option<(u16, &str)> {
54        if self.code != CODE_STATUS_CODE_AND_BODY {
55            return None;
56        }
57
58        if let Some(extra) = self.extra() {
59            if let Some(status_code) = extra.get("status_code").and_then(|x| x.as_i64()) {
60                if let Some(body) = extra.get("body").and_then(|x| x.as_str()) {
61                    return Some((status_code as u16, body));
62                }
63            }
64        }
65
66        None
67    }
68}
69
70impl Error {
71    pub fn is_error_validating_access_token(&self) -> bool {
72        self.message
73            .to_lowercase()
74            .contains("error validating access token")
75    }
76
77    pub fn is_access_token_session_has_been_invalidated(&self) -> bool {
78        self.message
79            .to_lowercase()
80            .contains("session has been invalidated")
81    }
82
83    pub fn is_access_token_session_has_expired(&self) -> bool {
84        self.message.to_lowercase().contains("session has expired")
85    }
86
87    pub fn is_access_token_session_key_is_malformed(&self) -> bool {
88        self.message
89            .to_lowercase()
90            .contains("session key is malformed")
91            || (self.message.to_lowercase().contains("session key ")
92                && self.message.to_lowercase().contains(" is malformed"))
93    }
94}
95
96#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, Clone)]
97pub enum ErrorType {
98    OAuthException,
99    GraphMethodException,
100    #[serde(other)]
101    Other(String),
102}
103
104impl core::fmt::Display for Error {
105    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106        write!(f, "{self:?}")
107    }
108}
109
110impl std::error::Error for Error {}
111
112//
113//
114//
115#[derive(Debug, Clone, Copy)]
116#[non_exhaustive]
117pub enum KnownErrorCase {
118    ApiTooManyCalls,
119    ApiUserTooManyCalls,
120    AccessTokenExpiredOrRevokedOrInvalid,
121    PermissionNotGrantedOrRemoved,
122    RetryLater,
123}
124
125impl KnownErrorCase {
126    pub fn is_api_too_many_calls(&self) -> bool {
127        matches!(self, Self::ApiTooManyCalls)
128    }
129
130    pub fn is_api_user_too_many_calls(&self) -> bool {
131        matches!(self, Self::ApiUserTooManyCalls)
132    }
133
134    pub fn is_access_token_expired_or_revoked_or_invalid(&self) -> bool {
135        matches!(self, Self::AccessTokenExpiredOrRevokedOrInvalid)
136    }
137
138    pub fn is_permission_not_granted_or_removed(&self) -> bool {
139        matches!(self, Self::PermissionNotGrantedOrRemoved)
140    }
141
142    pub fn is_retry_later(&self) -> bool {
143        matches!(self, Self::RetryLater)
144    }
145}
146
147impl core::fmt::Display for KnownErrorCase {
148    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
149        write!(f, "{self:?}")
150    }
151}
152
153impl std::error::Error for KnownErrorCase {}
154
155impl Error {
156    pub fn to_known_error_case(&self) -> Option<KnownErrorCase> {
157        if let Some(error_subcode) = self.error_subcode {
158            #[allow(clippy::single_match)]
159            match error_subcode {
160                463 | 467 => return Some(KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid),
161                _ => {}
162            }
163        }
164
165        match self.code {
166            102 => {
167                if self.error_subcode.is_none() {
168                    return Some(KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid);
169                }
170            }
171            2 => return Some(KnownErrorCase::RetryLater),
172            4 => return Some(KnownErrorCase::ApiTooManyCalls),
173            17 => return Some(KnownErrorCase::ApiUserTooManyCalls),
174            10 => return Some(KnownErrorCase::PermissionNotGrantedOrRemoved),
175            190 => return Some(KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid),
176            200..=299 => return Some(KnownErrorCase::PermissionNotGrantedOrRemoved),
177            _ => {}
178        }
179
180        None
181    }
182}
183
184impl From<&Error> for Option<KnownErrorCase> {
185    fn from(error: &Error) -> Self {
186        error.to_known_error_case()
187    }
188}
189
190impl From<Error> for Option<KnownErrorCase> {
191    fn from(error: Error) -> Self {
192        error.to_known_error_case()
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[derive(Deserialize, Debug)]
201    struct ResponseBodyErrJson {
202        error: Error,
203    }
204
205    #[test]
206    fn test_de_error() {
207        //
208        let content = include_str!(
209            "../tests/response_body_json_files/err__access_token_session_has_been_invalidated.json"
210        );
211        match serde_json::from_str::<ResponseBodyErrJson>(content) {
212            Ok(err_json) => {
213                // println!("{:?}", err_json);
214                assert!(matches!(
215                    err_json.error.to_known_error_case().unwrap(),
216                    KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
217                ));
218                assert!(err_json.error.is_error_validating_access_token());
219                assert!(err_json
220                    .error
221                    .is_access_token_session_has_been_invalidated());
222            }
223            Err(err) => panic!("{}", err),
224        }
225
226        //
227        let content = include_str!(
228            "../tests/response_body_json_files/err__access_token_session_has_expired.json"
229        );
230        match serde_json::from_str::<ResponseBodyErrJson>(content) {
231            Ok(err_json) => {
232                // println!("{:?}", err_json);
233                assert!(matches!(
234                    err_json.error.to_known_error_case().unwrap(),
235                    KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
236                ));
237                assert!(err_json.error.is_error_validating_access_token());
238                assert!(err_json.error.is_access_token_session_has_expired());
239            }
240            Err(err) => panic!("{}", err),
241        }
242
243        //
244        let content = include_str!(
245            "../tests/response_body_json_files/err__access_token_session_key_is_malformed.json"
246        );
247        match serde_json::from_str::<ResponseBodyErrJson>(content) {
248            Ok(err_json) => {
249                // println!("{:?}", err_json);
250                assert!(matches!(
251                    err_json.error.to_known_error_case().unwrap(),
252                    KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
253                ));
254                assert!(err_json.error.is_error_validating_access_token());
255                assert!(err_json.error.is_access_token_session_key_is_malformed());
256            }
257            Err(err) => panic!("{}", err),
258        }
259
260        /*
261        When https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token={short-lived-access-token}
262        */
263        let content = include_str!(
264            "../tests/response_body_json_files/err__access_token_session_key_x_is_malformed.json"
265        );
266        match serde_json::from_str::<ResponseBodyErrJson>(content) {
267            Ok(err_json) => {
268                // println!("{:?}", err_json);
269                assert!(matches!(
270                    err_json.error.to_known_error_case().unwrap(),
271                    KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
272                ));
273                assert!(err_json.error.is_error_validating_access_token());
274                assert!(err_json.error.is_access_token_session_key_is_malformed());
275            }
276            Err(err) => panic!("{}", err),
277        }
278
279        //
280        let content =
281            include_str!("../tests/response_body_json_files/err__access_token_unknown_1.json");
282        match serde_json::from_str::<ResponseBodyErrJson>(content) {
283            Ok(err_json) => {
284                // println!("{:?}", err_json);
285                assert!(matches!(
286                    err_json.error.to_known_error_case().unwrap(),
287                    KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
288                ));
289                assert!(!err_json.error.is_error_validating_access_token());
290            }
291            Err(err) => panic!("{}", err),
292        }
293    }
294}