autoendpoint/routers/
common.rs

1use crate::error::{ApiError, ApiResult};
2use crate::extractors::notification::Notification;
3use crate::headers::vapid::VapidHeaderWithKey;
4use crate::routers::RouterError;
5use actix_web::http::StatusCode;
6use autopush_common::db::client::DbClient;
7use autopush_common::metric_name::MetricName;
8use autopush_common::metrics::StatsdClientExt;
9use autopush_common::util::InsertOpt;
10use cadence::{Counted, StatsdClient, Timed};
11use std::collections::HashMap;
12use uuid::Uuid;
13
14use super::fcm::error::FcmError;
15
16/// Convert a notification into a WebPush message
17pub fn build_message_data(notification: &Notification) -> ApiResult<HashMap<&'static str, String>> {
18    let mut message_data = HashMap::new();
19    message_data.insert("chid", notification.subscription.channel_id.to_string());
20
21    // Only add the other headers if there's data
22    if let Some(data) = &notification.data {
23        message_data.insert("body", data.clone());
24        message_data.insert_opt("con", notification.headers.encoding.as_ref());
25        message_data.insert_opt("enc", notification.headers.encryption.as_ref());
26        message_data.insert_opt("cryptokey", notification.headers.crypto_key.as_ref());
27        message_data.insert_opt("enckey", notification.headers.encryption_key.as_ref());
28        // Report the data to the UA. How this value is reported back is still a work in progress, but
29        // we do set the state to "accepted" on desktop "ACK" at least.
30        trace!(
31            "🔍 Sending Reliability ID: {:?}",
32            notification.subscription.reliability_id
33        );
34        message_data.insert_opt("rid", notification.subscription.reliability_id.as_ref());
35    }
36
37    Ok(message_data)
38}
39
40/// Check the data against the max data size and return an error if there is too
41/// much data.
42pub fn message_size_check(data: &[u8], max_data: usize) -> Result<(), RouterError> {
43    if data.len() > max_data {
44        trace!("Data is too long by {} bytes", data.len() - max_data);
45        Err(RouterError::TooMuchData(data.len() - max_data))
46    } else {
47        Ok(())
48    }
49}
50
51/// Handle a bridge error by logging, updating metrics, etc
52/// This function uses the standard `slog` recording mechanisms and
53/// optionally calls a generic metric recording function for some
54/// types of errors. The error is returned by this function for later
55/// processing. This can include being called by the sentry middleware,
56/// which uses the `RecordableError` trait to optionally record metrics.
57/// see [autopush_common::middleware::sentry::SentryWrapperMiddleware].`call()` method
58pub async fn handle_error(
59    error: RouterError,
60    metrics: &StatsdClient,
61    db: &dyn DbClient,
62    platform: &str,
63    app_id: &str,
64    uaid: Uuid,
65    vapid: Option<VapidHeaderWithKey>,
66) -> ApiError {
67    match &error {
68        RouterError::Authentication => {
69            error!("Bridge authentication error");
70            incr_error_metric(
71                metrics,
72                platform,
73                app_id,
74                "authentication",
75                error.status(),
76                error.errno(),
77            );
78        }
79        RouterError::RequestTimeout => {
80            // Bridge timeouts are common.
81            info!("Bridge timeout");
82            incr_error_metric(
83                metrics,
84                platform,
85                app_id,
86                "timeout",
87                error.status(),
88                error.errno(),
89            );
90        }
91        RouterError::Connect(e) => {
92            warn!("Bridge unavailable: {}", e);
93            incr_error_metric(
94                metrics,
95                platform,
96                app_id,
97                "connection_unavailable",
98                error.status(),
99                error.errno(),
100            );
101        }
102        RouterError::NotFound => {
103            debug!("Bridge recipient not found, removing user");
104            incr_error_metric(
105                metrics,
106                platform,
107                app_id,
108                "recipient_gone",
109                error.status(),
110                error.errno(),
111            );
112
113            if let Err(e) = db.remove_user(&uaid).await {
114                warn!("Error while removing user due to bridge not_found: {}", e);
115            }
116        }
117        RouterError::TooMuchData(_) => {
118            // Do not log this error since it's fairly common.
119            incr_error_metric(
120                metrics,
121                platform,
122                app_id,
123                "too_much_data",
124                error.status(),
125                error.errno(),
126            );
127        }
128        RouterError::Fcm(FcmError::Upstream {
129            error_code: status, ..
130        }) => incr_error_metric(
131            metrics,
132            platform,
133            app_id,
134            &format!("upstream_{status}"),
135            error.status(),
136            error.errno(),
137        ),
138
139        _ => {
140            warn!("Unknown error while sending bridge request: {error}");
141            incr_error_metric(
142                metrics,
143                platform,
144                app_id,
145                "unknown",
146                error.status(),
147                error.errno(),
148            );
149        }
150    }
151
152    let mut err = ApiError::from(error);
153
154    if let Some(Ok(claims)) = vapid.map(|v| v.vapid.claims()) {
155        let mut extras = err.extras.unwrap_or_default();
156        if let Some(sub) = claims.sub {
157            extras.extend([("sub".to_owned(), sub)]);
158        }
159        err.extras = Some(extras);
160    };
161    err
162}
163
164/// Increment `notification.bridge.error`
165pub fn incr_error_metric(
166    metrics: &StatsdClient,
167    platform: &str,
168    app_id: &str,
169    reason: &str,
170    status: StatusCode,
171    errno: Option<usize>,
172) {
173    // I'd love to extract the status and errno from the passed ApiError, but a2 error handling makes that impossible.
174    metrics
175        .incr_with_tags(MetricName::NotificationBridgeError)
176        .with_tag("platform", platform)
177        .with_tag("app_id", app_id)
178        .with_tag("reason", reason)
179        .with_tag("error", &status.to_string())
180        .with_tag("errno", &errno.unwrap_or(0).to_string())
181        .send();
182}
183
184/// Update metrics after successfully routing the notification
185pub fn incr_success_metrics(
186    metrics: &StatsdClient,
187    platform: &str,
188    app_id: &str,
189    notification: &Notification,
190) {
191    metrics
192        .incr_with_tags(MetricName::NotificationBridgeSent)
193        .with_tag("platform", platform)
194        .with_tag("app_id", app_id)
195        .send();
196    metrics
197        .count_with_tags(
198            MetricName::NotificationMessageData.as_ref(),
199            notification.data.as_ref().map(String::len).unwrap_or(0) as i64,
200        )
201        .with_tag("platform", platform)
202        .with_tag("app_id", app_id)
203        .with_tag("destination", "Direct")
204        .send();
205    metrics
206        .time_with_tags(
207            MetricName::NotificationTotalRequestTime.as_ref(),
208            (autopush_common::util::sec_since_epoch() - notification.timestamp) * 1000,
209        )
210        .with_tag("platform", platform)
211        .with_tag("app_id", app_id)
212        .send();
213}
214
215/// Common router test code
216#[cfg(test)]
217pub mod tests {
218    use crate::extractors::notification::Notification;
219    use crate::extractors::notification_headers::NotificationHeaders;
220    use crate::extractors::routers::RouterType;
221    use crate::extractors::subscription::Subscription;
222    use autopush_common::db::User;
223    use std::collections::HashMap;
224    use uuid::Uuid;
225
226    pub const CHANNEL_ID: &str = "deadbeef-13f9-4639-87f9-2ff731824f34";
227
228    /// Get the test channel ID as a Uuid
229    pub fn channel_id() -> Uuid {
230        Uuid::parse_str(CHANNEL_ID).unwrap()
231    }
232
233    /// Create a notification
234    pub fn make_notification(
235        router_data: HashMap<String, serde_json::Value>,
236        data: Option<String>,
237        router_type: RouterType,
238    ) -> Notification {
239        let user = User::builder()
240            .router_data(router_data)
241            .router_type(router_type.to_string())
242            .build()
243            .unwrap();
244        Notification {
245            message_id: "test-message-id".to_string(),
246            subscription: Subscription {
247                user,
248                channel_id: channel_id(),
249                vapid: None,
250                reliability_id: None,
251            },
252            headers: NotificationHeaders {
253                ttl: 0,
254                topic: Some("test-topic".to_string()),
255                encoding: Some("test-encoding".to_string()),
256                encryption: Some("test-encryption".to_string()),
257                encryption_key: Some("test-encryption-key".to_string()),
258                crypto_key: Some("test-crypto-key".to_string()),
259            },
260            timestamp: 0,
261            sort_key_timestamp: 0,
262            data,
263            #[cfg(feature = "reliable_report")]
264            reliable_state: None,
265            #[cfg(feature = "reliable_report")]
266            reliability_id: None,
267        }
268    }
269}