Skip to main content

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}