Skip to content

fix(messaging, android): ignore non-message broadcasts in messaging receiver#9105

Merged
mikehardy merged 2 commits into
invertase:mainfrom
LinFeiLong:fix/messaging-android-filter-non-message-broadcasts
Jul 17, 2026
Merged

fix(messaging, android): ignore non-message broadcasts in messaging receiver#9105
mikehardy merged 2 commits into
invertase:mainfrom
LinFeiLong:fix/messaging-android-filter-non-message-broadcasts

Conversation

@LinFeiLong

@LinFeiLong LinFeiLong commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Description

ReactNativeFirebaseMessagingReceiver listens on the raw com.google.android.c2dm.intent.RECEIVE action and blindly converts every broadcast it receives into a RemoteMessage emitted to JS (onMessageReceived / background handler). That channel is not message-only, and this produces phantom empty messages in JS.

Root cause (verified in both SDKs' sources). The official FirebaseMessagingService (handleMessageIntent) applies two routing guards before ever calling onMessageReceived:

  1. message_type present and ≠ "gcm" → routed to dedicated callbacks (onDeletedMessages, onMessageSent, onSendError) — these are control broadcasts, not messages;
  2. intents that are not FCM-delivered messages are never surfaced as messages. Every message actually delivered by FCM carries a server-stamped google.message_id.

The RNFB receiver has neither guard. The most visible consequence: since firebase/firebase-android-sdk#5410 (firebase-messaging 23.3.0+, late 2023), when FCM displays a notification itself, it attaches a NOTIFICATION_DISMISS analytics intent as deleteIntent, wrapped as an implicit package-scoped broadcast on that same c2dm action. So every notification swipe-to-dismiss fabricates an empty RemoteMessage (no messageId, no data, no notification) delivered to the app's JS message handlers. This is exactly what #8040 reported (closed stale without a fix).

Real-world impact: in our production app (~25k Android devices), these phantom messages hit our push payload validation and accounted for ~600k error logs / 30 days — ~23% of all app error logs — drowning real signal.

Fix. Reproduce the two entry guards of the official routing at the top of onReceive:

  • message_type present and ≠ "gcm" → ignore;
  • no google.message_id extra → ignore (not an FCM-delivered message).

The guards are safe by construction: every FCM-delivered message (data-only, notification, high/normal priority, console tests) carries google.message_id. The official pipeline (notification display, dismiss analytics, onDeletedMessages) is untouched — it flows through the Firebase SDK's own FirebaseInstanceIdReceiver, which keeps receiving the same broadcasts.

Related issues

Release Summary

fix(messaging, android): ignore non-message c2dm broadcasts (control broadcasts, notification-dismiss analytics intents) instead of emitting them to JS as empty messages

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
    • Other (macOS, web)
  • My change includes tests;
    • e2e tests added or updated in packages/\*\*/e2e
    • jest tests added or updated in packages/\*\*/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan

No automated tests added: the package has no Android unit-test harness, and the e2e suite has no mechanism to inject com.google.android.c2dm.intent.RECEIVE broadcasts (the dismiss deleteIntent is fired by the OS notification tray, which Detox cannot drive). Happy to add coverage if you can point me at a preferred approach.

Manual repro (matches #8040):

  1. Send a notification-type message (e.g. from the Firebase console) while the app is foregrounded, so FCM displays the notification itself.
  2. Swipe the notification away.
  3. Before this change: onMessageReceived fires with an empty RemoteMessage (no messageId, no data, no notification). After: nothing is emitted, and logcat shows broadcast ignored, no google.message_id (not an FCM message).

Field evidence — we ship this exact change as a patch-package patch on @react-native-firebase/messaging@25.1.0 in our app; verified on real devices:

  • Swiping an FCM-displayed notification no longer emits an empty message to JS (logcat shows the ignore guard firing instead).
  • Data-only pushes, notification pushes, and Firebase console test messages all still arrive — they all carry google.message_id.
  • onDeletedMessages still works: the Firebase SDK's own FirebaseInstanceIdReceiver receives the same broadcasts and routes deleted_messages through FirebaseMessagingService, which RNFB subclasses.

🔥

…eceiver

ReactNativeFirebaseMessagingReceiver listens on the raw
com.google.android.c2dm.intent.RECEIVE channel and converts every
broadcast into a RemoteMessage emitted to JS. That channel also carries
control broadcasts (deleted_messages, send_event, send_error) and, since
firebase-android-sdk#5410, the wrapped NOTIFICATION_DISMISS analytics
intent that FCM attaches as deleteIntent to notifications it displays
itself - so every notification swipe surfaces an empty message to JS.

Reproduce the two entry guards of the official FirebaseMessagingService
routing: ignore broadcasts with a non-gcm message_type, and ignore
broadcasts without a google.message_id (every FCM-delivered message
carries one, server-stamped).

Fixes invertase#8040
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue where the Android messaging receiver incorrectly processes non-message broadcasts as valid FCM messages. By introducing routing guards that verify the message type and the presence of a message ID, the change ensures that only legitimate FCM messages are surfaced to the JavaScript layer, significantly reducing noise and error logs in production environments.

Highlights

  • Broadcast Filtering: Implemented routing guards in the Android messaging receiver to ignore non-FCM broadcasts, preventing phantom empty messages from being emitted to JavaScript.
  • Root Cause Resolution: Added checks for 'message_type' and 'google.message_id' to mirror the official FirebaseMessagingService behavior, effectively filtering out control broadcasts and notification-dismiss analytics intents.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces routing guards to ReactNativeFirebaseMessagingReceiver to ignore non-message intents and control broadcasts, aligning its behavior with the official FirebaseMessagingService. The feedback suggests using intent.getStringExtra(key) instead of chaining intent.getExtras().getString(key) to make the code more idiomatic and concise.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Address review feedback: intent.getStringExtra(key) is the idiomatic
form and equivalent to intent.getExtras().getString(key).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikehardy mikehardy added plugin: messaging FCM only - ( messaging() ) - do not use for Notifications Workflow: Needs Review Pending feedback or review from a maintainer. labels Jul 16, 2026

@mikehardy mikehardy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

interesting - this looks valid to me - thanks for taking the time to post a PR - I appreciate it

CI chewing on it now

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.11%. Comparing base (a095bd0) to head (44b674a).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9105      +/-   ##
============================================
- Coverage     65.11%   65.11%   -0.00%     
+ Complexity     1833     1832       -1     
============================================
  Files           503      503              
  Lines         39180    39187       +7     
  Branches       5807     5745      -62     
============================================
+ Hits          25510    25512       +2     
+ Misses        12235    12200      -35     
- Partials       1435     1475      +40     
Flag Coverage Δ
android-native 60.47% <0.00%> (-0.04%) ⬇️
e2e-ts-android 58.15% <0.00%> (-0.02%) ⬇️
e2e-ts-ios 62.82% <ø> (+0.03%) ⬆️
e2e-ts-macos 49.97% <ø> (-<0.01%) ⬇️
ios-native 62.82% <ø> (+0.03%) ⬆️
jest 49.58% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mikehardy mikehardy removed the Workflow: Needs Review Pending feedback or review from a maintainer. label Jul 17, 2026
@mikehardy
mikehardy merged commit da17208 into invertase:main Jul 17, 2026
22 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plugin: messaging FCM only - ( messaging() ) - do not use for Notifications

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛] An empty remote message is displayed if the notification is swiped while the app is open.

2 participants