From 3cbfacb13c68cb93066268fb05844f8fefb27ba6 Mon Sep 17 00:00:00 2001 From: ezhilnn Date: Tue, 23 Jun 2026 02:10:48 +0530 Subject: [PATCH] fix: remove artificial 500 message limit from sendEach --- .../firebase/messaging/FirebaseMessaging.java | 39 ++++++++++++------- .../messaging/FirebaseMessagingIT.java | 16 ++++++++ .../messaging/FirebaseMessagingTest.java | 24 ++++++------ 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index bb7b8208f..06ac51d0b 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -152,7 +152,8 @@ protected String execute() throws FirebaseMessagingException { *

The list of responses obtained by calling {@link BatchResponse#getResponses()} on the return * value is in the same order as the input list. * - * @param messages A non-null, non-empty list containing up to 500 messages. + * @param messages A non-null, non-empty list of messages. For very large lists, consider + * chunking into smaller batches to avoid FCM server-side rate limiting. * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for * delivery. An exception here or a {@link BatchResponse} with all failures indicates a total @@ -163,7 +164,6 @@ public BatchResponse sendEach(@NonNull List messages) throws FirebaseMe return sendEach(messages, false); } - /** * Sends each message in the given list via Firebase Cloud Messaging. * Unlike {@link #sendAll(List)}, this method makes an HTTP call for each message in the @@ -177,7 +177,8 @@ public BatchResponse sendEach(@NonNull List messages) throws FirebaseMe *

The list of responses obtained by calling {@link BatchResponse#getResponses()} on the return * value is in the same order as the input list. * - * @param messages A non-null, non-empty list containing up to 500 messages. + * @param messages A non-null, non-empty list of messages. For very large lists, consider + * chunking into smaller batches to avoid FCM server-side rate limiting. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for @@ -197,7 +198,8 @@ public BatchResponse sendEach( /** * Similar to {@link #sendEach(List)} but performs the operation asynchronously. * - * @param messages A non-null, non-empty list containing up to 500 messages. + * @param messages A non-null, non-empty list of messages. For very large lists, consider + * chunking into smaller batches to avoid FCM server-side rate limiting. * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent. */ @@ -208,7 +210,8 @@ public ApiFuture sendEachAsync(@NonNull List messages) { /** * Similar to {@link #sendEach(List, boolean)} but performs the operation asynchronously. * - * @param messages A non-null, non-empty list containing up to 500 messages. + * @param messages A non-null, non-empty list of messages. For very large lists, consider + * chunking into smaller batches to avoid FCM server-side rate limiting. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent. @@ -217,14 +220,16 @@ public ApiFuture sendEachAsync(@NonNull List messages, b return sendEachOpAsync(messages, dryRun); } - // Returns an ApiFuture directly since this function is non-blocking. Individual child send + // Returns an ApiFuture directly since this function is non-blocking. Individual child send // requests are still called async and run in background threads. private ApiFuture sendEachOpAsync( final List messages, final boolean dryRun) { final List immutableMessages = ImmutableList.copyOf(messages); checkArgument(!immutableMessages.isEmpty(), "messages list must not be empty"); - checkArgument(immutableMessages.size() <= 500, - "messages list must not contain more than 500 elements"); + // NOTE: The 500-message limit that existed here was inherited from the deprecated sendAll() + // method, which used a single HTTP batch request limited by Google's batch API. Since + // sendEach() makes individual HTTP calls per message, no such limit applies. Callers should + // chunk very large lists themselves to avoid FCM server-side rate limiting. List> list = new ArrayList<>(); for (Message message : immutableMessages) { @@ -232,7 +237,7 @@ private ApiFuture sendEachOpAsync( ApiFuture messageId = sendOpForSendResponse(message, dryRun).callAsync(app); list.add(messageId); } - + // Gather all futures and combine into a list ApiFuture> responsesFuture = ApiFutures.allAsList(list); @@ -240,7 +245,7 @@ private ApiFuture sendEachOpAsync( // the main thread. This uses the current thread to execute, but since the transformation // function is non-blocking the transformation itself is also non-blocking. return ApiFutures.transform( - responsesFuture, + responsesFuture, (responses) -> { return new BatchResponseImpl(responses); }, @@ -272,7 +277,8 @@ protected SendResponse execute() { * {@link BatchResponse#getResponses()} on the return value is in the same order as the * tokens in the {@link MulticastMessage}. * - * @param message A non-null {@link MulticastMessage} + * @param message A non-null {@link MulticastMessage}. For very large token lists, consider + * chunking into smaller batches to avoid FCM server-side rate limiting. * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for * delivery. An exception here or a {@link BatchResponse} with all failures indicates a total @@ -297,7 +303,8 @@ public BatchResponse sendEachForMulticast( * {@link BatchResponse#getResponses()} on the return value is in the same order as the * tokens in the {@link MulticastMessage}. * - * @param message A non-null {@link MulticastMessage}. + * @param message A non-null {@link MulticastMessage}. For very large token lists, consider + * chunking into smaller batches to avoid FCM server-side rate limiting. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for @@ -315,7 +322,8 @@ public BatchResponse sendEachForMulticast(@NonNull MulticastMessage message, boo * Similar to {@link #sendEachForMulticast(MulticastMessage)} but performs the operation * asynchronously. * - * @param message A non-null {@link MulticastMessage}. + * @param message A non-null {@link MulticastMessage}. For very large token lists, consider + * chunking into smaller batches to avoid FCM server-side rate limiting. * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent. */ @@ -327,7 +335,8 @@ public ApiFuture sendEachForMulticastAsync(@NonNull MulticastMess * Similar to {@link #sendEachForMulticast(MulticastMessage, boolean)} but performs the operation * asynchronously. * - * @param message A non-null {@link MulticastMessage}. + * @param message A non-null {@link MulticastMessage}. For very large token lists, consider + * chunking into smaller batches to avoid FCM server-side rate limiting. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent. @@ -677,4 +686,4 @@ FirebaseMessaging build() { return new FirebaseMessaging(this); } } -} +} \ No newline at end of file diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index e084b8c29..2c76e2adf 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -169,6 +169,22 @@ public void testSendFiveHundredWithSendEach() throws Exception { assertNull(sendResponse.getException()); } } + + @Test + public void testSendMoreThanFiveHundredWithSendEach() throws Exception { + List messages = new ArrayList<>(); + for (int i = 0; i < 501; i++) { + messages.add(Message.builder().setTopic("foo-bar-" + (i % 10)).build()); + } + + // Previously this would throw IllegalArgumentException due to the 500 limit. + // After removing the limit, this should succeed. + BatchResponse response = FirebaseMessaging.getInstance().sendEach(messages, true); + + assertEquals(501, response.getResponses().size()); + assertEquals(501, response.getSuccessCount()); + assertEquals(0, response.getFailureCount()); + } @Test public void testSendEachForMulticast() throws Exception { diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 42a499b75..fc7891ba5 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -38,6 +38,7 @@ import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.Nullable; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -305,22 +306,21 @@ public void testSendEachWithEmptyList() throws FirebaseMessagingException { } @Test - public void testSendEachWithTooManyMessages() throws FirebaseMessagingException { - MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); - FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - ImmutableList.Builder listBuilder = ImmutableList.builder(); + public void testSendEachWithMoreThanFiveHundredMessages() { + List messages = new ArrayList<>(); for (int i = 0; i < 501; i++) { - listBuilder.add(Message.builder().setTopic("topic").build()); + messages.add(Message.builder().setTopic("foo-bar").build()); } - + // Previously threw IllegalArgumentException due to artificial 500 limit. + // sendEach() makes individual HTTP calls per message, so no such limit applies. + // Verify that 501 messages no longer throws an IllegalArgumentException. try { - messaging.sendEach(listBuilder.build(), false); - fail("No error thrown for too many messages in the list"); - } catch (IllegalArgumentException expected) { - // expected + FirebaseMessaging.getInstance().sendEach(messages); + } catch (IllegalArgumentException e) { + fail("sendEach() should not throw IllegalArgumentException for more than 500 messages"); + } catch (Exception e) { + // Other exceptions (e.g. network) are acceptable in unit test context } - - assertNull(client.lastMessage); } @Test