1use crate::headers::vapid::VapidError;
4use crate::routers::RouterError;
5use actix_web::{
6 HttpResponse, Result,
7 dev::ServiceResponse,
8 error::{JsonPayloadError, PayloadError, ResponseError},
9 http::StatusCode,
10 http::header::{CacheControl, CacheDirective},
11 middleware::ErrorHandlerResponse,
12};
13use actix_http::header;
15use backtrace::Backtrace;
16use rand::RngExt;
17use serde::ser::SerializeMap;
18use serde::{Serialize, Serializer};
19use std::error::Error;
20use std::fmt::{self, Display};
21use thiserror::Error;
22use validator::{ValidationErrors, ValidationErrorsKind};
23
24use autopush_common::{db::error::DbError, errors::ReportableError};
25
26pub type ApiResult<T> = Result<T, ApiError>;
28
29const ERROR_URL: &str = "http://autopush.readthedocs.io/en/latest/http.html#error-codes";
31const RETRY_AFTER_PERIOD: u64 = 120; const RETRY_AFTER_JITTER: u64 = 30;
36
37fn jittered_retry_after() -> u64 {
44 rand::rng().random_range(
45 RETRY_AFTER_PERIOD - RETRY_AFTER_JITTER..=RETRY_AFTER_PERIOD + RETRY_AFTER_JITTER,
46 )
47}
48
49#[derive(Debug)]
51pub struct ApiError {
52 pub kind: ApiErrorKind,
53 pub backtrace: Backtrace,
54 pub extras: Option<Vec<(String, String)>>,
55}
56
57impl ApiError {
58 pub fn render_404<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
61 Ok(autopush_common::errors::render_404(res).unwrap())
63 }
64}
65
66#[derive(Debug, Error)]
68pub enum ApiErrorKind {
69 #[error(transparent)]
70 Io(#[from] std::io::Error),
71
72 #[error(transparent)]
73 Metrics(#[from] cadence::MetricError),
74
75 #[error(transparent)]
76 Validation(#[from] validator::ValidationErrors),
77
78 #[error(transparent)]
79 PayloadError(actix_web::Error),
80
81 #[error(transparent)]
82 VapidError(#[from] VapidError),
83
84 #[error(transparent)]
85 Router(#[from] RouterError),
86
87 #[error(transparent)]
88 Jwt(#[from] jsonwebtoken::errors::Error),
89
90 #[error(transparent)]
91 Serde(#[from] serde_json::Error),
92
93 #[error(transparent)]
94 ReqwestError(#[from] reqwest::Error),
95
96 #[error("Error while validating token")]
97 TokenHashValidation(#[source] openssl::error::ErrorStack),
98
99 #[error("Error while creating secret")]
100 RegistrationSecretHash(#[source] openssl::error::ErrorStack),
101
102 #[error("Error while creating endpoint URL: {0}")]
103 EndpointUrl(#[source] autopush_common::errors::ApcError),
104
105 #[error("Database error: {0}")]
106 Database(#[from] DbError),
107
108 #[error("Conditional database operation failed: {0}")]
109 Conditional(String),
110
111 #[error("Invalid token")]
112 InvalidToken,
113
114 #[error("UAID not found")]
115 NoUser,
116
117 #[error("No such subscription")]
118 NoSubscription,
119
120 #[error("{0}")]
122 InvalidEncryption(String),
123
124 #[error("Invalid API version")]
126 InvalidApiVersion,
127
128 #[error("Missing TTL value")]
129 NoTTL,
130
131 #[error("Invalid router type")]
132 InvalidRouterType,
133
134 #[error("Invalid router token")]
135 InvalidRouterToken,
136
137 #[error("Invalid message ID")]
138 InvalidMessageId,
139
140 #[error("Invalid Authentication")]
141 InvalidAuthentication,
142
143 #[error("Invalid Local Auth {0}")]
144 InvalidLocalAuth(String),
145
146 #[error("General error {0}")]
147 General(String),
148
149 #[error("ERROR:Success")]
150 LogCheck,
151}
152
153impl ApiErrorKind {
154 pub fn retry_after(&self) -> Option<u64> {
159 match self {
160 ApiErrorKind::Router(e) => e.retry_after(),
161 _ => None,
162 }
163 }
164
165 pub fn status(&self) -> StatusCode {
167 match self {
168 ApiErrorKind::PayloadError(e) => e.as_response_error().status_code(),
169 ApiErrorKind::Router(e) => e.status(),
170
171 ApiErrorKind::Validation(_)
172 | ApiErrorKind::InvalidEncryption(_)
173 | ApiErrorKind::NoTTL
174 | ApiErrorKind::InvalidRouterType
175 | ApiErrorKind::InvalidRouterToken
176 | ApiErrorKind::InvalidMessageId => StatusCode::BAD_REQUEST,
177
178 ApiErrorKind::VapidError(_)
179 | ApiErrorKind::Jwt(_)
180 | ApiErrorKind::Serde(_)
181 | ApiErrorKind::TokenHashValidation(_)
182 | ApiErrorKind::InvalidAuthentication
183 | ApiErrorKind::InvalidLocalAuth(_) => StatusCode::UNAUTHORIZED,
184
185 ApiErrorKind::InvalidToken | ApiErrorKind::InvalidApiVersion => StatusCode::NOT_FOUND,
186
187 ApiErrorKind::NoUser | ApiErrorKind::NoSubscription => StatusCode::GONE,
188
189 ApiErrorKind::LogCheck => StatusCode::IM_A_TEAPOT,
190
191 ApiErrorKind::Conditional(_) => StatusCode::SERVICE_UNAVAILABLE,
192
193 ApiErrorKind::Database(e) => e.status(),
194
195 ApiErrorKind::General(_)
196 | ApiErrorKind::Io(_)
197 | ApiErrorKind::Metrics(_)
198 | ApiErrorKind::EndpointUrl(_)
199 | ApiErrorKind::RegistrationSecretHash(_)
200 | ApiErrorKind::ReqwestError(_) => StatusCode::INTERNAL_SERVER_ERROR,
201 }
202 }
203
204 pub fn metric_label(&self) -> Option<&'static str> {
206 Some(match self {
207 ApiErrorKind::PayloadError(_) => "payload_error",
208 ApiErrorKind::Router(e) => return e.metric_label(),
209
210 ApiErrorKind::Validation(_) => "validation",
211 ApiErrorKind::InvalidEncryption(_) => "invalid_encryption",
212 ApiErrorKind::NoTTL => "no_ttl",
213 ApiErrorKind::InvalidRouterType => "invalid_router_type",
214 ApiErrorKind::InvalidRouterToken => "invalid_router_token",
215 ApiErrorKind::InvalidMessageId => "invalid_message_id",
216
217 ApiErrorKind::VapidError(_) => "vapid_error",
218 ApiErrorKind::Jwt(_) | ApiErrorKind::Serde(_) => "jwt",
219 ApiErrorKind::TokenHashValidation(_) => "token_hash_validation",
220 ApiErrorKind::InvalidAuthentication => "invalid_authentication",
221 ApiErrorKind::InvalidLocalAuth(_) => "invalid_local_auth",
222
223 ApiErrorKind::InvalidToken => "invalid_token",
224 ApiErrorKind::InvalidApiVersion => "invalid_api_version",
225
226 ApiErrorKind::NoUser => "no_user",
227 ApiErrorKind::NoSubscription => "no_subscription",
228
229 ApiErrorKind::LogCheck => "log_check",
230
231 ApiErrorKind::General(_) => "general",
232 ApiErrorKind::Io(_) => "io",
233 ApiErrorKind::Metrics(_) => "metrics",
234 ApiErrorKind::Database(e) => return e.metric_label(),
235 ApiErrorKind::Conditional(_) => "conditional",
236 ApiErrorKind::EndpointUrl(e) => return e.metric_label(),
237 ApiErrorKind::RegistrationSecretHash(_) => "registration_secret_hash",
238 ApiErrorKind::ReqwestError(_) => "reqwest",
239 })
240 }
241
242 pub fn is_sentry_event(&self) -> bool {
244 match self {
245 ApiErrorKind::Router(e) => e.is_sentry_event(),
247 ApiErrorKind::Database(e) => e.is_sentry_event(),
248 ApiErrorKind::NoTTL | ApiErrorKind::InvalidEncryption(_) |
250 ApiErrorKind::VapidError(_)
252 | ApiErrorKind::Jwt(_)
253 | ApiErrorKind::TokenHashValidation(_)
254 | ApiErrorKind::InvalidAuthentication
255 | ApiErrorKind::InvalidLocalAuth(_) |
256 ApiErrorKind::NoUser | ApiErrorKind::NoSubscription |
258 ApiErrorKind::PayloadError(_) |
260 ApiErrorKind::Validation(_) |
261 ApiErrorKind::Conditional(_) |
262 ApiErrorKind::ReqwestError(_) => false,
263 _ => true,
264 }
265 }
266
267 pub fn errno(&self) -> Option<usize> {
269 match self {
270 ApiErrorKind::Router(e) => e.errno(),
271
272 ApiErrorKind::Validation(e) => errno_from_validation_errors(e),
273
274 ApiErrorKind::InvalidToken | ApiErrorKind::InvalidApiVersion => Some(102),
275
276 ApiErrorKind::NoUser => Some(103),
277
278 ApiErrorKind::PayloadError(error)
279 if matches!(error.as_error(), Some(PayloadError::Overflow))
280 || matches!(error.as_error(), Some(JsonPayloadError::Overflow { .. })) =>
281 {
282 Some(104)
283 }
284
285 ApiErrorKind::NoSubscription => Some(106),
286
287 ApiErrorKind::InvalidRouterType => Some(108),
288
289 ApiErrorKind::VapidError(_)
290 | ApiErrorKind::TokenHashValidation(_)
291 | ApiErrorKind::Jwt(_)
292 | ApiErrorKind::Serde(_)
293 | ApiErrorKind::InvalidAuthentication
294 | ApiErrorKind::InvalidLocalAuth(_) => Some(109),
295
296 ApiErrorKind::InvalidEncryption(_) => Some(110),
297
298 ApiErrorKind::NoTTL => Some(111),
299
300 ApiErrorKind::LogCheck => Some(999),
301
302 ApiErrorKind::General(_)
303 | ApiErrorKind::Io(_)
304 | ApiErrorKind::Metrics(_)
305 | ApiErrorKind::Database(_)
306 | ApiErrorKind::Conditional(_)
307 | ApiErrorKind::PayloadError(_)
308 | ApiErrorKind::InvalidRouterToken
309 | ApiErrorKind::RegistrationSecretHash(_)
310 | ApiErrorKind::EndpointUrl(_)
311 | ApiErrorKind::InvalidMessageId
312 | ApiErrorKind::ReqwestError(_) => None,
313 }
314 }
315}
316
317impl Display for ApiError {
318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319 self.kind.fmt(f)
320 }
321}
322
323impl Error for ApiError {
324 fn source(&self) -> Option<&(dyn Error + 'static)> {
325 self.kind.source()
326 }
327}
328
329impl<T> From<T> for ApiError
332where
333 ApiErrorKind: From<T>,
334{
335 fn from(item: T) -> Self {
336 ApiError {
337 kind: ApiErrorKind::from(item),
338 backtrace: Backtrace::new_unresolved(),
339 extras: None,
340 }
341 }
342}
343
344impl ResponseError for ApiError {
345 fn status_code(&self) -> StatusCode {
346 self.kind.status()
347 }
348
349 fn error_response(&self) -> HttpResponse {
350 let mut builder = HttpResponse::build(self.kind.status());
351
352 match self.status_code() {
353 StatusCode::GONE => {
354 builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400)]));
355 }
356 StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => {
357 let retry_after = self.kind.retry_after().unwrap_or_else(jittered_retry_after);
358 builder.insert_header((header::RETRY_AFTER, retry_after.to_string()));
359 }
360 _ => {}
361 }
362
363 builder.json(self)
364 }
365}
366
367impl Serialize for ApiError {
368 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
369 where
370 S: Serializer,
371 {
372 let status = self.kind.status();
373 let mut map = serializer.serialize_map(Some(5))?;
374
375 map.serialize_entry("code", &status.as_u16())?;
376 map.serialize_entry("errno", &self.kind.errno())?;
377 map.serialize_entry("error", &status.canonical_reason())?;
378 map.serialize_entry("message", &self.kind.to_string())?;
379 map.serialize_entry("more_info", ERROR_URL)?;
380 map.end()
381 }
382}
383
384impl ReportableError for ApiError {
385 fn reportable_source(&self) -> Option<&(dyn ReportableError + 'static)> {
386 match &self.kind {
387 ApiErrorKind::EndpointUrl(e) => Some(e),
388 ApiErrorKind::Database(e) => Some(e),
389 _ => None,
390 }
391 }
392
393 fn backtrace(&self) -> Option<&Backtrace> {
394 Some(&self.backtrace)
395 }
396
397 fn is_sentry_event(&self) -> bool {
398 self.kind.is_sentry_event()
399 }
400
401 fn metric_label(&self) -> Option<&'static str> {
402 self.kind.metric_label()
403 }
404
405 fn extras(&self) -> Vec<(&str, String)> {
406 let mut extras: Vec<(&str, String)> = match &self.extras {
407 Some(extras) => extras.iter().map(|e| (e.0.as_str(), e.1.clone())).collect(),
408 None => Default::default(),
409 };
410
411 match &self.kind {
412 ApiErrorKind::Router(e) => extras.extend(e.extras()),
413 ApiErrorKind::LogCheck => extras.extend(vec![("coffee", "Unsupported".to_owned())]),
414 _ => {}
415 };
416 extras
417 }
418}
419
420fn errno_from_validation_errors(e: &ValidationErrors) -> Option<usize> {
423 e.errors()
425 .values()
426 .flat_map(|error| match error {
427 ValidationErrorsKind::Struct(inner_errors) => {
428 Box::new(errno_from_validation_errors(inner_errors).into_iter())
429 as Box<dyn Iterator<Item = usize>>
430 }
431 ValidationErrorsKind::List(indexed_errors) => Box::new(
432 indexed_errors
433 .values()
434 .filter_map(|errors| errno_from_validation_errors(errors)),
435 )
436 as Box<dyn Iterator<Item = usize>>,
437 ValidationErrorsKind::Field(errors) => {
438 Box::new(errors.iter().filter_map(|error| error.code.parse().ok()))
439 as Box<dyn Iterator<Item = usize>>
440 }
441 })
442 .next()
443}
444
445#[cfg(test)]
446mod tests {
447 use actix_web::ResponseError;
448 use autopush_common::{db::error::DbError, sentry::event_from_error};
449 use std::collections::HashSet;
450
451 use crate::routers::RouterError;
452 use crate::routers::fcm::error::FcmError;
453
454 use super::{
455 ApiError, ApiErrorKind, RETRY_AFTER_JITTER, RETRY_AFTER_PERIOD, header,
456 jittered_retry_after,
457 };
458 use crate::error::ReportableError;
459
460 #[test]
461 fn sentry_event_with_extras() {
462 let dbe = DbError::Integrity("foo".to_owned(), Some("bar".to_owned()));
463 let e: ApiError = ApiErrorKind::Database(dbe).into();
464 let event = event_from_error(&e);
465 assert_eq!(event.exception.len(), 2);
466 assert_eq!(event.exception[0].ty, "Integrity");
467 assert_eq!(event.exception[1].ty, "ApiError");
468 assert_eq!(event.extra.get("row"), Some(&"bar".into()));
469 }
470
471 #[test]
474 fn jittered_retry_after_spreads_within_band() {
475 let band =
476 (RETRY_AFTER_PERIOD - RETRY_AFTER_JITTER)..=(RETRY_AFTER_PERIOD + RETRY_AFTER_JITTER);
477 let values: HashSet<u64> = (0..200).map(|_| jittered_retry_after()).collect();
478
479 for value in &values {
480 assert!(band.contains(value), "{value} outside {band:?}");
481 }
482 assert!(values.len() > 1, "no jitter applied");
485 }
486
487 #[test]
490 fn retry_after_header_prefers_upstream() {
491 let e: ApiError = ApiErrorKind::Router(RouterError::Fcm(FcmError::Upstream {
492 error_code: "RESOURCE_EXHAUSTED".to_owned(),
493 message: "quota".to_owned(),
494 retry_after: Some(45),
495 }))
496 .into();
497 let response = e.error_response();
498
499 assert_eq!(response.status().as_u16(), 429);
500 assert_eq!(
501 response.headers().get(header::RETRY_AFTER).unwrap(),
502 "45",
503 "upstream Retry-After should not be jittered"
504 );
505 }
506
507 #[test]
509 fn retry_after_header_falls_back_to_jitter() {
510 let e: ApiError = ApiErrorKind::Router(RouterError::Fcm(FcmError::Upstream {
511 error_code: "UNAVAILABLE".to_owned(),
512 message: "try later".to_owned(),
513 retry_after: None,
514 }))
515 .into();
516 let response = e.error_response();
517
518 assert_eq!(response.status().as_u16(), 503);
519 let secs: u64 = response
520 .headers()
521 .get(header::RETRY_AFTER)
522 .expect("a Retry-After header")
523 .to_str()
524 .unwrap()
525 .parse()
526 .expect("delta-seconds");
527 assert!(
528 ((RETRY_AFTER_PERIOD - RETRY_AFTER_JITTER)..=(RETRY_AFTER_PERIOD + RETRY_AFTER_JITTER))
529 .contains(&secs),
530 "secs = {secs}"
531 );
532 }
533
534 #[cfg(feature = "bigtable")]
536 #[test]
537 fn test_label_for_metrics() {
538 let e: ApiError = ApiErrorKind::Database(DbError::BTError(
540 autopush_common::db::bigtable::BigTableError::PoolTimeout(
541 deadpool::managed::TimeoutType::Create,
542 ),
543 ))
544 .into();
545
546 assert_eq!(
548 e.kind.metric_label(),
549 Some("storage.bigtable.error.pool_timeout")
550 );
551
552 assert_eq!(e.kind.status(), actix_http::StatusCode::SERVICE_UNAVAILABLE)
554 }
555
556 #[tokio::test]
558 async fn pass_extras() {
559 let e = RouterError::NotFound;
560 let mut ae = ApiError::from(e);
561 ae.extras = Some([("foo".to_owned(), "bar".to_owned())].to_vec());
562
563 let aex: Vec<(&str, String)> = ae.extras();
564 assert!(aex.contains(&("foo", "bar".to_owned())));
565
566 let e = ApiErrorKind::LogCheck;
567 let mut ae = ApiError::from(e);
568 ae.extras = Some([("foo".to_owned(), "bar".to_owned())].to_vec());
569
570 let aex: Vec<(&str, String)> = ae.extras();
571 assert!(aex.contains(&("foo", "bar".to_owned())));
572 assert!(aex.contains(&("coffee", "Unsupported".to_owned())));
573 }
574}