From ebf38c5179c5089b42f5fd248f653c17ab1fed91 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Tue, 22 Sep 2026 13:36:21 +0530 Subject: [PATCH 1/2] fix(fcm): reject empty message batches before dispatch Validate empty lists in both send paths and cover direct and multicast callers with regression tests. --- firebase_admin/messaging.py | 14 ++++++++++---- tests/test_messaging.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 93ecfda1..2dda267a 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -136,7 +136,7 @@ def send_each( recipients. Instead, FCM performs all the usual validations and emulates the send operation. Args: - messages: A list of ``messaging.Message`` instances. + messages: A non-empty list of up to 500 ``messaging.Message`` instances. dry_run: A boolean indicating whether to run the operation in dry run mode (optional). app: An App instance (optional). @@ -160,7 +160,7 @@ async def send_each_async( recipients. Instead, FCM performs all the usual validations and emulates the send operation. Args: - messages: A list of ``messaging.Message`` instances. + messages: A non-empty list of up to 500 ``messaging.Message`` instances. dry_run: A boolean indicating whether to run the operation in dry run mode (optional). app: An App instance (optional). @@ -217,7 +217,8 @@ async def send_each_for_multicast_async( recipients. Instead, FCM performs all the usual validations and emulates the send operation. Args: - multicast_message: An instance of ``messaging.MulticastMessage``. + multicast_message: An instance of ``messaging.MulticastMessage`` with at least one + token or fid. dry_run: A boolean indicating whether to run the operation in dry run mode (optional). app: An App instance (optional). @@ -238,7 +239,8 @@ def send_each_for_multicast(multicast_message, dry_run=False, app=None): recipients. Instead, FCM performs all the usual validations and emulates the send operation. Args: - multicast_message: An instance of ``messaging.MulticastMessage``. + multicast_message: An instance of ``messaging.MulticastMessage`` with at least one + token or fid. dry_run: A boolean indicating whether to run the operation in dry run mode (optional). app: An App instance (optional). @@ -445,6 +447,8 @@ def send_each(self, messages: List[Message], dry_run: bool = False) -> BatchResp """Sends the given messages to FCM via the FCM v1 API.""" if not isinstance(messages, list): raise ValueError('messages must be a list of messaging.Message instances.') + if not messages: + raise ValueError('messages must not be empty.') if len(messages) > 500: raise ValueError('messages must not contain more than 500 elements.') @@ -473,6 +477,8 @@ async def send_each_async(self, messages: List[Message], dry_run: bool = True) - """Sends the given messages to FCM via the FCM v1 API.""" if not isinstance(messages, list): raise ValueError('messages must be a list of messaging.Message instances.') + if not messages: + raise ValueError('messages must not be empty.') if len(messages) > 500: raise ValueError('messages must not contain more than 500 elements.') diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 749e5311..973a5e95 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -1953,6 +1953,15 @@ def test_invalid_send_each(self, msg): expected = 'messages must be a list of messaging.Message instances.' assert str(excinfo.value) == expected + def test_send_each_empty_batch(self): + with pytest.raises(ValueError, match='messages must not be empty.'): + messaging.send_each([]) + + @pytest.mark.asyncio + async def test_send_each_async_empty_batch(self): + with pytest.raises(ValueError, match='messages must not be empty.'): + await messaging.send_each_async([]) + def test_invalid_over_500(self): msg = messaging.Message(topic='foo') with pytest.raises(ValueError) as excinfo: @@ -2273,6 +2282,17 @@ def test_invalid_send_each_for_multicast(self, msg): expected = 'Message must be an instance of messaging.MulticastMessage class.' assert str(excinfo.value) == expected + def test_send_each_for_multicast_empty_batch(self): + msg = messaging.MulticastMessage(tokens=[]) + with pytest.raises(ValueError, match='messages must not be empty.'): + messaging.send_each_for_multicast(msg) + + @pytest.mark.asyncio + async def test_send_each_for_multicast_async_empty_batch(self): + msg = messaging.MulticastMessage(tokens=[]) + with pytest.raises(ValueError, match='messages must not be empty.'): + await messaging.send_each_for_multicast_async(msg) + def test_send_each_for_multicast(self): payload1 = json.dumps({'name': 'message-id1'}) payload2 = json.dumps({'name': 'message-id2'}) From ebf718a04909281d83fa301b4c7689aca3881c00 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Tue, 22 Sep 2026 14:33:49 +0530 Subject: [PATCH 2/2] fix(fcm): report empty multicast recipients clearly --- firebase_admin/messaging.py | 2 ++ tests/test_messaging.py | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 2dda267a..14f5944f 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -203,6 +203,8 @@ def _get_messages_from_multicast(multicast_message: MulticastMessage) -> List[Me fid=fid ) for fid in multicast_message.fids]) + if not messages: + raise ValueError('multicast_message must contain at least one token or fid.') return messages async def send_each_for_multicast_async( diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 973a5e95..3730d9cb 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -2282,15 +2282,21 @@ def test_invalid_send_each_for_multicast(self, msg): expected = 'Message must be an instance of messaging.MulticastMessage class.' assert str(excinfo.value) == expected - def test_send_each_for_multicast_empty_batch(self): - msg = messaging.MulticastMessage(tokens=[]) - with pytest.raises(ValueError, match='messages must not be empty.'): + @pytest.mark.parametrize('recipients', [{'tokens': []}, {'fids': []}, + {'tokens': [], 'fids': []}]) + def test_send_each_for_multicast_empty_batch(self, recipients): + msg = messaging.MulticastMessage(**recipients) + with pytest.raises( + ValueError, match='multicast_message must contain at least one token or fid.'): messaging.send_each_for_multicast(msg) @pytest.mark.asyncio - async def test_send_each_for_multicast_async_empty_batch(self): - msg = messaging.MulticastMessage(tokens=[]) - with pytest.raises(ValueError, match='messages must not be empty.'): + @pytest.mark.parametrize('recipients', [{'tokens': []}, {'fids': []}, + {'tokens': [], 'fids': []}]) + async def test_send_each_for_multicast_async_empty_batch(self, recipients): + msg = messaging.MulticastMessage(**recipients) + with pytest.raises( + ValueError, match='multicast_message must contain at least one token or fid.'): await messaging.send_each_for_multicast_async(msg) def test_send_each_for_multicast(self):