firebase_admin/messaging/error.rs
1//! Error types for the `messaging` module.
2
3use crate::core::CoreError;
4use serde::Deserialize;
5
6/// The top-level error type for all `messaging` module operations.
7#[derive(Debug, thiserror::Error)]
8pub enum MessagingError {
9 /// A lower-level, service-independent error occurred.
10 #[error(transparent)]
11 Core(#[from] CoreError),
12
13 /// The underlying HTTP request failed.
14 #[error("HTTP request failed: {0}")]
15 Http(#[from] reqwest::Error),
16
17 /// The FCM v1 API returned an error response.
18 #[error("FCM API error ({status}): {message}")]
19 Api {
20 /// HTTP status code returned by the API.
21 status: u16,
22 /// Human-readable error message returned by the API.
23 message: String,
24 /// Machine-readable error code, when the API provides one (e.g.
25 /// `UNREGISTERED`, `INVALID_ARGUMENT`, `QUOTA_EXCEEDED`).
26 error_code: Option<String>,
27 },
28
29 /// More than [`crate::messaging::MAX_BATCH_SIZE`] messages/tokens were
30 /// passed to a batch operation
31 /// ([`crate::messaging::MessagingClient::send_each`] or
32 /// [`crate::messaging::MessagingClient::send_each_for_multicast`]).
33 #[error("batch size {actual} exceeds the maximum of {max} messages/tokens per call")]
34 BatchTooLarge {
35 /// The number of messages/tokens that were passed in.
36 actual: usize,
37 /// The maximum allowed per call.
38 max: usize,
39 },
40}
41
42/// The `{"error": {...}}` envelope Google APIs use for error responses.
43///
44/// See <https://cloud.google.com/apis/design/errors#http_mapping>. FCM v1
45/// additionally nests a well-known short status string (e.g. `UNREGISTERED`,
46/// `SENDER_ID_MISMATCH`, `QUOTA_EXCEEDED`) inside `error.details`, as an
47/// object whose `@type` ends in `FcmError` and carries an `errorCode` field.
48/// Some error responses (e.g. auth/permission failures) omit that `details`
49/// entry entirely; those still carry a top-level `error.status` gRPC
50/// canonical code (`NOT_FOUND`, `PERMISSION_DENIED`, ...), which the official
51/// Admin SDKs fall back to (`getErrorCode` in `messaging-errors-internal.ts`)
52/// — mirrored here in [`MessagingError::from_api_response`].
53#[derive(Debug, Deserialize)]
54struct GoogleApiErrorBody {
55 error: GoogleApiError,
56}
57
58#[derive(Debug, Deserialize)]
59struct GoogleApiError {
60 message: String,
61 /// The gRPC canonical status string, e.g. `NOT_FOUND`,
62 /// `INVALID_ARGUMENT`, `PERMISSION_DENIED`.
63 status: Option<String>,
64 #[serde(default)]
65 details: Vec<GoogleApiErrorDetail>,
66}
67
68#[derive(Debug, Deserialize)]
69struct GoogleApiErrorDetail {
70 #[serde(rename = "errorCode")]
71 error_code: Option<String>,
72}
73
74/// Turns a [`reqwest::Response`] into a parsed value, or a
75/// [`MessagingError::Api`]/[`MessagingError::Core`] if the request failed or
76/// the success body couldn't be deserialized.
77///
78/// Centralizes the "check status, read body, extract FCM's error code" logic
79/// shared by every FCM v1 call site, so error-code parsing only needs to be
80/// correct in one place — mirrors
81/// `crate::auth::error::parse_identity_toolkit_response`.
82pub(crate) async fn parse_fcm_response<T: serde::de::DeserializeOwned>(
83 response: reqwest::Response,
84) -> Result<T, MessagingError> {
85 if !response.status().is_success() {
86 let status = response.status().as_u16();
87 let body = response
88 .text()
89 .await
90 .unwrap_or_else(|_| "<no response body>".to_string());
91 return Err(MessagingError::from_api_response(status, &body));
92 }
93
94 response
95 .json::<T>()
96 .await
97 .map_err(|e| MessagingError::Core(CoreError::Http(e)))
98}
99
100impl MessagingError {
101 /// Builds a [`MessagingError::Api`] from a non-success HTTP response,
102 /// extracting FCM's well-known short error code from the response
103 /// body. Prefers the FCM-specific `error.details[].errorCode` (e.g.
104 /// `UNREGISTERED`); when that's absent, falls back to the gRPC
105 /// canonical `error.status` (e.g. `NOT_FOUND`), matching the official
106 /// Admin SDKs' fallback order.
107 fn from_api_response(status: u16, body: &str) -> Self {
108 let (message, error_code) = match serde_json::from_str::<GoogleApiErrorBody>(body) {
109 Ok(parsed) => {
110 let code = parsed
111 .error
112 .details
113 .iter()
114 .find_map(|d| d.error_code.clone())
115 .or_else(|| parsed.error.status.clone());
116 (parsed.error.message, code)
117 }
118 Err(_) => (body.to_string(), None),
119 };
120
121 MessagingError::Api {
122 status,
123 message,
124 error_code,
125 }
126 }
127}