Skip to content

Added minimal support for sending FCM messages in async using HTTP/2 #870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions firebase_admin/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

import json
from platform import python_version
from typing import Callable, Optional

import google.auth
import requests
import httpx

import firebase_admin
from firebase_admin import exceptions
Expand Down Expand Up @@ -128,6 +130,36 @@ def handle_platform_error_from_requests(error, handle_func=None):

return exc if exc else _handle_func_requests(error, message, error_dict)

def handle_platform_error_from_httpx(
error: httpx.HTTPError,
handle_func: Optional[Callable[..., Optional[exceptions.FirebaseError]]] = None
) -> exceptions.FirebaseError:
"""Constructs a ``FirebaseError`` from the given httpx error.

This can be used to handle errors returned by Google Cloud Platform (GCP) APIs.

Args:
error: An error raised by the httpx module while making an HTTP call to a GCP API.
handle_func: A function that can be used to handle platform errors in a custom way. When
specified, this function will be called with three arguments. It has the same
signature as ```_handle_func_httpx``, but may return ``None``.

Returns:
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
"""

if isinstance(error, httpx.HTTPStatusError):
response = error.response
content = response.content.decode()
status_code = response.status_code
error_dict, message = _parse_platform_error(content, status_code)
exc = None
if handle_func:
exc = handle_func(error, message, error_dict)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discussed offline, let's confirm if this complex code path is necessary for error handling. If it is not, let's clean it up and keep it simple :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we use this map FCM specific errors like:

FCM_ERROR_TYPES = {
        'APNS_AUTH_ERROR': ThirdPartyAuthError,
        'QUOTA_EXCEEDED': QuotaExceededError,
        'SENDER_ID_MISMATCH': SenderIdMismatchError,
        'THIRD_PARTY_AUTH_ERROR': ThirdPartyAuthError,
        'UNREGISTERED': UnregisteredError,
}

We map to these errors by parsing the error response details. The default handle_func (handle_X_error) is agnostic to the source service of the error and doesn't try to parse based the error response details. FCM is the only service that currently makes use of this optional handle_func but this seems like the correct way to support this scenario.

We could move the logic from the helper function _handle_func_X to handle_platform_error_from_X.

Will merge as is and we can revisit this in a follow up PR if necessary.


return exc if exc else _handle_func_httpx(error, message, error_dict)
return handle_httpx_error(error)


def handle_operation_error(error):
"""Constructs a ``FirebaseError`` from the given operation error.
Expand Down Expand Up @@ -204,6 +236,60 @@ def handle_requests_error(error, message=None, code=None):
err_type = _error_code_to_exception_type(code)
return err_type(message=message, cause=error, http_response=error.response)

def _handle_func_httpx(error: httpx.HTTPError, message, error_dict) -> exceptions.FirebaseError:
"""Constructs a ``FirebaseError`` from the given GCP error.

Args:
error: An error raised by the httpx module while making an HTTP call.
message: A message to be included in the resulting ``FirebaseError``.
error_dict: Parsed GCP error response.

Returns:
FirebaseError: A ``FirebaseError`` that can be raised to the user code or None.
"""
code = error_dict.get('status')
return handle_httpx_error(error, message, code)


def handle_httpx_error(error: httpx.HTTPError, message=None, code=None) -> exceptions.FirebaseError:
"""Constructs a ``FirebaseError`` from the given httpx error.

This method is agnostic of the remote service that produced the error, whether it is a GCP
service or otherwise. Therefore, this method does not attempt to parse the error response in
any way.

Args:
error: An error raised by the httpx module while making an HTTP call.
message: A message to be included in the resulting ``FirebaseError`` (optional). If not
specified the string representation of the ``error`` argument is used as the message.
code: A GCP error code that will be used to determine the resulting error type (optional).
If not specified the HTTP status code on the error response is used to determine a
suitable error code.

Returns:
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
"""
if isinstance(error, httpx.TimeoutException):
return exceptions.DeadlineExceededError(
message='Timed out while making an API call: {0}'.format(error),
cause=error)
if isinstance(error, httpx.ConnectError):
return exceptions.UnavailableError(
message='Failed to establish a connection: {0}'.format(error),
cause=error)
if isinstance(error, httpx.HTTPStatusError):
print("printing status error", error)
if not code:
code = _http_status_to_error_code(error.response.status_code)
if not message:
message = str(error)

err_type = _error_code_to_exception_type(code)
return err_type(message=message, cause=error, http_response=error.response)

return exceptions.UnknownError(
message='Unknown error while making a remote service call: {0}'.format(error),
cause=error)

def _http_status_to_error_code(status):
"""Maps an HTTP status to a platform error code."""
Expand Down
Loading