fcm/client/response.rs
1pub use chrono::{DateTime, Duration, FixedOffset};
2use serde::Deserialize;
3use std::{error::Error, fmt, str::FromStr};
4
5/// A description of what went wrong with the push notification.
6/// Referred from [Firebase documentation](https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9)
7#[derive(Deserialize, Debug, PartialEq, Copy, Clone)]
8pub enum ErrorReason {
9 /// Check that the request contains a registration token (in the `to` or
10 /// `registration_ids` field).
11 MissingRegistration,
12
13 /// Check the format of the registration token you pass to the server. Make
14 /// sure it matches the registration token the client app receives from
15 /// registering with Firebase Notifications. Do not truncate or add
16 /// additional characters.
17 InvalidRegistration,
18
19 /// An existing registration token may cease to be valid in a number of
20 /// scenarios, including:
21 ///
22 /// * If the client app unregisters with FCM.
23 /// * If the client app is automatically unregistered, which can happen if
24 /// the user uninstalls the application. For example, on iOS, if the APNS
25 /// Feedback Service reported the APNS token as invalid.
26 /// * If the registration token expires (for example, Google might decide to
27 /// refresh registration tokens, or the APNS token has expired for iOS
28 /// devices).
29 /// * If the client app is updated but the new version is not configured to
30 /// receive messages.
31 ///
32 /// For all these cases, remove this registration token from the app server
33 /// and stop using it to send messages.
34 NotRegistered,
35
36 /// Make sure the message was addressed to a registration token whose
37 /// package name matches the value passed in the request.
38 InvalidPackageName,
39
40 /// A registration token is tied to a certain group of senders. When a
41 /// client app registers for FCM, it must specify which senders are allowed
42 /// to send messages. You should use one of those sender IDs when sending
43 /// messages to the client app. If you switch to a different sender, the
44 /// existing registration tokens won't work.
45 MismatchSenderId,
46
47 /// Check that the provided parameters have the right name and type.
48 InvalidParameters,
49
50 /// Check that the total size of the payload data included in a message does
51 /// not exceed FCM limits: 4096 bytes for most messages, or 2048 bytes in
52 /// the case of messages to topics. This includes both the keys and the
53 /// values.
54 MessageTooBig,
55
56 /// Check that the custom payload data does not contain a key (such as
57 /// `from`, or `gcm`, or any value prefixed by google) that is used
58 /// internally by FCM. Note that some words (such as `collapse_key`) are
59 /// also used by FCM but are allowed in the payload, in which case the
60 /// payload value will be overridden by the FCM value.
61 InvalidDataKey,
62
63 /// Check that the value used in `time_to_live` is an integer representing a
64 /// duration in seconds between 0 and 2,419,200 (4 weeks).
65 InvalidTtl,
66
67 /// In internal use only. Check
68 /// [FcmError::ServerError](enum.FcmError.html#variant.ServerError).
69 Unavailable,
70
71 /// In internal use only. Check
72 /// [FcmError::ServerError](enum.FcmError.html#variant.ServerError).
73 InternalServerError,
74
75 /// The rate of messages to a particular device is too high. If an iOS app
76 /// sends messages at a rate exceeding APNs limits, it may receive this
77 /// error message
78 ///
79 /// Reduce the number of messages sent to this device and use exponential
80 /// backoff to retry sending.
81 DeviceMessageRateExceeded,
82
83 /// The rate of messages to subscribers to a particular topic is too high.
84 /// Reduce the number of messages sent for this topic and use exponential
85 /// backoff to retry sending.
86 TopicsMessageRateExceeded,
87
88 /// A message targeted to an iOS device could not be sent because the
89 /// required APNs authentication key was not uploaded or has expired. Check
90 /// the validity of your development and production credentials.
91 InvalidApnsCredential,
92}
93
94#[derive(Deserialize, Debug)]
95pub struct FcmResponse {
96 pub message_id: Option<u64>,
97 pub error: Option<ErrorReason>,
98 pub multicast_id: Option<i64>,
99 pub success: Option<u64>,
100 pub failure: Option<u64>,
101 pub canonical_ids: Option<u64>,
102 pub results: Option<Vec<MessageResult>>,
103}
104
105#[derive(Deserialize, Debug)]
106pub struct MessageResult {
107 pub message_id: Option<String>,
108 pub registration_id: Option<String>,
109 pub error: Option<ErrorReason>,
110}
111
112/// Fatal errors. Referred from [Firebase
113/// documentation](https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9)
114#[derive(PartialEq, Debug)]
115pub enum FcmError {
116 /// The sender account used to send a message couldn't be authenticated. Possible causes are:
117 ///
118 /// Authorization header missing or with invalid syntax in HTTP request.
119 ///
120 /// * The Firebase project that the specified server key belongs to is
121 /// incorrect.
122 /// * Legacy server keys only—the request originated from a server not
123 /// whitelisted in the Server key IPs.
124 ///
125 /// Check that the token you're sending inside the Authentication header is
126 /// the correct Server key associated with your project. See Checking the
127 /// validity of a Server key for details. If you are using a legacy server
128 /// key, you're recommended to upgrade to a new key that has no IP
129 /// restrictions.
130 Unauthorized,
131
132 /// Check that the JSON message is properly formatted and contains valid
133 /// fields (for instance, making sure the right data type is passed in).
134 InvalidMessage(String),
135
136 /// The server couldn't process the request. Retry the same request, but you must:
137 ///
138 /// * Honor the [RetryAfter](enum.RetryAfter.html) value if included.
139 /// * Implement exponential back-off in your retry mechanism. (e.g. if you
140 /// waited one second before the first retry, wait at least two second
141 /// before the next one, then 4 seconds and so on). If you're sending
142 /// multiple messages, delay each one independently by an additional random
143 /// amount to avoid issuing a new request for all messages at the same time.
144 ///
145 /// Senders that cause problems risk being blacklisted.
146 ServerError(Option<RetryAfter>),
147}
148
149impl Error for FcmError {}
150
151impl fmt::Display for FcmError {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 match self {
154 FcmError::Unauthorized => write!(f, "authorization header missing or with invalid syntax in HTTP request"),
155 FcmError::InvalidMessage(ref s) => write!(f, "invalid message {}", s),
156 FcmError::ServerError(_) => write!(f, "the server couldn't process the request"),
157 }
158 }
159}
160
161impl From<reqwest::Error> for FcmError {
162 fn from(_: reqwest::Error) -> Self {
163 Self::ServerError(None)
164 }
165}
166
167#[derive(PartialEq, Debug)]
168pub enum RetryAfter {
169 /// Amount of time to wait until retrying the message is allowed.
170 Delay(Duration),
171
172 /// A point in time until retrying the message is allowed.
173 DateTime(DateTime<FixedOffset>),
174}
175
176impl FromStr for RetryAfter {
177 type Err = crate::Error;
178
179 fn from_str(s: &str) -> Result<Self, Self::Err> {
180 s.parse::<i64>()
181 .map(Duration::seconds)
182 .map(RetryAfter::Delay)
183 .or_else(|_| DateTime::parse_from_rfc2822(s).map(RetryAfter::DateTime))
184 .map_err(|e| crate::Error::InvalidMessage(format!("{}", e)))
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use chrono::{DateTime, Duration};
192 use serde_json::json;
193
194 #[test]
195 fn test_some_errors() {
196 let errors = vec![
197 ("MissingRegistration", ErrorReason::MissingRegistration),
198 ("InvalidRegistration", ErrorReason::InvalidRegistration),
199 ("NotRegistered", ErrorReason::NotRegistered),
200 ("InvalidPackageName", ErrorReason::InvalidPackageName),
201 ("MismatchSenderId", ErrorReason::MismatchSenderId),
202 ("InvalidParameters", ErrorReason::InvalidParameters),
203 ("MessageTooBig", ErrorReason::MessageTooBig),
204 ("InvalidDataKey", ErrorReason::InvalidDataKey),
205 ("InvalidTtl", ErrorReason::InvalidTtl),
206 ("Unavailable", ErrorReason::Unavailable),
207 ("InternalServerError", ErrorReason::InternalServerError),
208 ("DeviceMessageRateExceeded", ErrorReason::DeviceMessageRateExceeded),
209 ("TopicsMessageRateExceeded", ErrorReason::TopicsMessageRateExceeded),
210 ("InvalidApnsCredential", ErrorReason::InvalidApnsCredential),
211 ];
212
213 for (error_str, error_enum) in errors.into_iter() {
214 let response_data = json!({
215 "error": error_str,
216 "results": [
217 {"error": error_str}
218 ]
219 });
220
221 let response_string = serde_json::to_string(&response_data).unwrap();
222 let fcm_response: FcmResponse = serde_json::from_str(&response_string).unwrap();
223
224 assert_eq!(Some(error_enum.clone()), fcm_response.results.unwrap()[0].error,);
225
226 assert_eq!(Some(error_enum), fcm_response.error,)
227 }
228 }
229
230 #[test]
231 fn test_retry_after_from_seconds() {
232 assert_eq!(RetryAfter::Delay(Duration::seconds(420)), "420".parse().unwrap());
233 }
234
235 #[test]
236 fn test_retry_after_from_date() {
237 let date = "Sun, 06 Nov 1994 08:49:37 GMT";
238 let retry_after = RetryAfter::from_str(date).unwrap();
239
240 assert_eq!(
241 RetryAfter::DateTime(DateTime::parse_from_rfc2822(date).unwrap()),
242 retry_after,
243 );
244 }
245}