Repository navigation
fix(messaging,ios): deliver background messages exactly once and stop dropping them at startup - #18830
Conversation
… dropping them at startup Three bugs made `onBackgroundMessage` (and `onMessage`) unreliable on Apple platforms: - Every push was handled twice on iOS: the plugin is both a GoogleUtilities app delegate interceptor and a Flutter application delegate, and the GoogleUtilities proxy calls both. Messages are now deduplicated by `gcm.message_id`, and duplicate deliveries still complete so the proxy can call the system completion handler. - A background message that reached Dart before `onBackgroundMessage` was registered (e.g. background launch for a push, with `FirebaseMessaging.instance` used first) was silently dropped. It now waits up to 10 seconds for the handler. - `onMessage` / `onMessageOpenedApp` received nothing until another messaging API had been called, because the Dart channel handler was only attached then. Listening to them now attaches it. The plugin also enlarges the channel buffer so messages sent before Dart is ready are not overwritten by the next one. Covered by unit tests and by new iOS e2e tests that deliver pushes through the app delegate like iOS does (`simctl push` cannot reach that path).
Using Gemini Code AssistThe 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
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 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. |
- Only drop the duplicate call within one delivery from iOS: the message ID is forgotten on the next main loop turn, so a later push that reuses a `gcm.message_id` (pushes sent straight to APNs) is handled again. macOS has no duplicate path and no longer dedupes. - Enlarge the channel buffer with every message sent to Dart. Flutter drops messages until the engine runs Dart, which is when UIScene apps register plugins, so doing it at registration had no effect. - With `FirebaseAppDelegateProxyEnabled=NO` the remote notification handler is copied onto the app delegate; forward to the plugin instead of running with the app delegate as `self` (crashed with the new dedupe, undefined behavior before). - Only hold background messages without a handler during the first 10 seconds after the app starts using messaging, so apps that never register one don't keep every push open. - Wake held messages from the `onBackgroundMessage` setter, whatever the platform instance is. - Listening to `onMessage` without a binding no longer throws.
…xy disabled - Move the iOS delivery tests to `remote_notification_delivery.dart`, run by `e2e_test.dart` and on their own by `remote_notification_delivery_test.dart`. - New `ios-app-delegate-proxy-disabled` job in `e2e-messaging` runs them with `FirebaseAppDelegateProxyEnabled=false`, through a new `info-plist-overrides` input of `reusable_e2e_ios.yaml`. That configuration takes another path in the plugin (its handler is copied onto the app delegate). - New e2e test: a background message that arrives before `onBackgroundMessage` is set runs the handler once it is, and the push completes once with `NewData` (fails on main: the handler never runs).
- Only hold a background message for apps that register a handler: the plugin remembers whether Dart registered one this run or the previous one and says so before each background message. Apps without an iOS handler no longer keep cold background launches open for 10 seconds. - Only hold background messages on iOS and macOS. - With `FirebaseAppDelegateProxyEnabled=NO`, hand the copied remote notification handler's call to Flutter's forwarding first, so the other plugins still get it. - e2e: messages sent before Dart handles the channel are all kept (fails without the channel buffer resize), and the startup race test registers a handler first like an app that uses one.
- Handle `Messaging#backgroundMessageHandlerRegistered` before touching FIRMessaging: apps register the handler before configuring Firebase, and this logged that Firebase was not configured. - The first run of this version counts as having a background handler, so an update background-launched by a push still holds the message; the flag is reset instead of removed. - Dart does not hold messages unless the native side says the app has a handler, so older native code gets the old behavior. - Forward with the literal selector rather than `_cmd`. - CI: fail the build step when an Info.plist override is missing from the built app, rather than silently testing the default configuration. - Docs: say messages only wait for apps that registered a handler.
Description
Reproduced the reports in #17206 in the iOS simulator by delivering pushes through
UIApplication.delegatethe way iOS does (simctl pushnever reachesapplication:didReceiveRemoteNotification:fetchCompletionHandler:). Three bugs:1. Every push was handled twice on iOS. The plugin is both a GoogleUtilities app delegate interceptor and a Flutter application delegate (
addApplicationDelegate, #13525). The GoogleUtilities proxy calls the interceptor and forwards toFlutterAppDelegate, which calls the plugin again, soonMessageandonBackgroundMessagefired twice per push. The second call within the same delivery is now dropped bygcm.message_id(the ID is forgotten on the next main loop turn, so a later push that reuses it is still handled), and still calls its completion handler, otherwise the proxy never calls the system one.2. Background messages were dropped at startup. When a push arrives as the app starts (background launch), it can reach Dart before
main()has calledFirebaseMessaging.onBackgroundMessage. If the app usedFirebaseMessaging.instancefirst (e.g.getToken()/requestPermission()), the channel handler was already attached and the message was handed to anullhandler: no error, no handler call. It now waits for a handler during the first 10 seconds after the app starts using messaging (native completes the push after 25 seconds). It only waits for apps that register one: the plugin remembers whether Dart registered a handler during this run or the previous one, and says so before each background message.3.
onMessage/onMessageOpenedAppreceived nothing until another messaging API was called. The Dart channel handler was only attached whenFirebaseMessagingPlatform.instancewas first used, whichFirebaseMessaging.instanceand the streams don't do. Listening to either stream now attaches it.The plugin also enlarges the channel buffer (Flutter keeps a single message by default) so messages sent before Dart is ready are not overwritten. It is sent with every message, because Flutter drops messages until the engine runs Dart, which is when UIScene apps register plugins.
Also fixed: with
FirebaseAppDelegateProxyEnabled=NO, the plugin copies itsdidReceiveRemoteNotification:fetchCompletionHandler:onto the app delegate, which then ran plugin code with the app delegate asself. It now hands the call to Flutter's forwarding (so other plugins still get it), or else to the plugin. Verified on the simulator with the proxy disabled, and now covered in CI (below).getToken)Messages sent to Dart before it handles the channel are now kept (up to 64) instead of all but the last being dropped, so an app that starts using messaging late gets the messages received in between.
Tests
firebase_messaging_platform_interface): messages arriving before the handler are held and handled, the 10 s startup window, waking from the setter,onMessageworks without any other API call, and listening without a binding. They fail onmain.example/integration_test/e2e_test.dart, run bye2e-messagingon the simulator): a test-only channel in the example app (RemoteNotificationTestChannel.swift, liketests/permissionson Android) delivers data messages through the app delegate in foreground and background. They checkonMessage/onBackgroundMessagerun exactly once, the push completes once withNewData, and a later push reusing a message ID is handled again. The once-only tests fail onmain(each message received twice); all pass with this change on an iOS 27 simulator, also withFirebaseAppDelegateProxyEnabled=NO.CI
integration_test/remote_notification_delivery.dart. They are run bye2e_test.dart, and on their own by a newios-app-delegate-proxy-disabledjob ine2e-messagingwithFirebaseAppDelegateProxyEnabled=false, through a newinfo-plist-overridesinput ofreusable_e2e_ios.yaml.onBackgroundMessageis set runs it once it is (fails on main);Related Issues
Related to #17206 (core cause of the "background handler not called" reports), probably #18776 and #17339.
Checklist
///).melos run analyze) does not report any problems on my PR.Breaking Change