Skip to content

[🐛] iOS: onMessage never fires in foreground when APNs payload has content-available: 0 #9228

Description

@lnuvy

Issue

Summary

On iOS, messaging().onMessage() is never called for a foreground push whose APNs payload explicitly sets aps.content-available: 0.

  • The same push is delivered and displayed correctly in the background and quit states, and tapping it works.
  • The same app receives onMessage normally for foreground pushes that omit the content-available key.
  • Nothing arrives at JS at all: no event, no error, nothing in the notification center either (since the app never gets a chance to present it).

content-available: 0 is valid APNs usage — it is an explicit declaration that the push is not a silent push. Any sender that builds its own aps dictionary and serializes that field as 0 hits this. We first saw it after integrating a third‑party marketing push provider that sets mutable-content: 1 and assembles apns.payload.aps itself; our own backend pushes (FCM notification block only, no content-available key) were unaffected, which is why it went unnoticed.

Usage

import { getMessaging, onMessage } from '@react-native-firebase/messaging';

useEffect(() => {
  const unsubscribe = onMessage(getMessaging(), async remoteMessage => {
    console.log('foreground message', remoteMessage); // never logged for content-available: 0
  });
  return unsubscribe;
}, []);

Reproduction

Send two foreground pushes to the same iOS device (physical device — the simulator cannot receive remote pushes) and observe onMessage:

# Payload onMessage
1 {"aps":{"alert":{"title":"a","body":"b"}}} fires ✅
2 {"aps":{"alert":{"title":"a","body":"b"},"content-available":0}} never fires

Both payloads carry gcm.message_id, so both pass the outer guard in willPresentNotification; only the inner contentAvailable check differs.

Via FCM HTTP v1 the second payload is produced by:

{
  "message": {
    "token": "<device token>",
    "apns": {
      "payload": {
        "aps": {
          "alert": { "title": "a", "body": "b" },
          "content-available": 0
        }
      }
    }
  }
}

Root cause

RNFBMessagingSerializer stores contentAvailable whenever the key is present, regardless of its value:

// packages/messaging/ios/RNFBMessaging/RNFBMessagingSerializer.m
// message.contentAvailable
if (apsDict[@"content-available"] != nil) {
  message[@"contentAvailable"] = @([RCTConvert BOOL:apsDict[@"content-available"]]);
}

// message.mutableContent  <- note: this one compares the value
if (apsDict[@"mutable-content"] != nil && [apsDict[@"mutable-content"] intValue] == 1) {

So content-available: 0 yields @(NO) — a non-nil NSNumber.

willPresentNotification then guards event emission on key presence rather than on the value:

// packages/messaging/ios/RNFBMessaging/RNFBMessaging+UNUserNotificationCenter.m:122
// Don't send an event if contentAvailable is true -
// application:didReceiveRemoteNotification will send the event for us, we
// don't want to duplicate them
if (!notificationDict[@"contentAvailable"]) {
  [[RNFBRCTEventEmitter shared] sendEventWithName:@"messaging_message_received"
                                             body:notificationDict];
}

@0 is a non-nil object, so !notificationDict[@"contentAvailable"] is false and messaging_message_received is never emitted. The comment directly above states the intent in terms of the value ("if contentAvailable is true"), so this is an implementation/intent mismatch rather than deliberate behaviour.

This guard is older than PR #5604 — that PR actually removed it and it was restored by the revert in #5641 (Aug 2021). It has not been revisited since, including through the willPresentNotification changes this year (#8786, #9094, #8945). The code path is unchanged on main at the time of writing.

Fix

Compare the value, matching the stated intent. One line:

-    if (!notificationDict[@"contentAvailable"]) {
+    if (![notificationDict[@"contentAvailable"] boolValue]) {

Genuine silent pushes (content-available: 1) are still skipped, preserving the de‑duplication with application:didReceiveRemoteNotification:.

PR with this one-line fix follows (linked below).

Verified on a physical device before/after the change by streaming the native delegate with xcrun devicectl device process launch --console: before, the content-available: 0 payload reaches willPresentNotification but no event is emitted; after, onMessage fires and the serialized message carries "contentAvailable": false — confirming the serializer had been producing a correct dictionary all along and only the emission guard was wrong.

Workaround (until released)

Patch the one line above with patch-package / pnpm patchedDependencies. There is no JS-side workaround: the decision not to emit lives inside the native bridge, so onMessage subscribers never see an event that was never sent.

Possibly related

Same symptom string, but those threads never identified a payload-level cause and some describe a different failure mode (provisional authorization / token registration timing), so I am not claiming this explains them: #7772, #6107, #4513.


Project Files

Javascript

Click To Expand

package.json:

{
  "dependencies": {
    "react": "19.0.0",
    "react-native": "0.78.x",
    "@react-native-firebase/app": "19.3.0",
    "@react-native-firebase/messaging": "19.3.0"
  }
}

(Trimmed to the relevant packages. The bug is independent of app configuration — it is a payload-value check in the native bridge, reproducible with the two payloads above alone.)

firebase.json for react-native-firebase v6:

# N/A — defaults; no messaging-related keys set

iOS

Click To Expand

ios/Podfile:

  • I'm not using Pods
  • I'm using Pods and my Podfile looks like:
# Standard RN 0.78 Podfile; use_frameworks! :linkage => :static
# No RNFB-specific overrides. Not relevant to this issue (native bridge value check).

AppDelegate.m:

// Standard RN 0.78 AppDelegate (Swift). No custom UNUserNotificationCenterDelegate —
// RNFBMessaging's own delegate is the one handling willPresentNotification.


Android

Click To Expand

Have you converted to AndroidX?

  • my application is an AndroidX application?
  • I am using android/gradle.settings jetifier=true for Android compatibility?
  • I am using the NPM package jetifier for react-native compatibility?

android/build.gradle:

// N/A — iOS-only issue (the affected code is in the iOS bridge)

android/app/build.gradle:

// N/A

android/settings.gradle:

// N/A

MainApplication.java:

// N/A

AndroidManifest.xml:

<!-- N/A -->


Environment

Click To Expand

react-native info output:

System:
  OS: macOS 26.x
Binaries:
  Node: 22.x
IDEs:
  Xcode: 26.x
npmPackages:
  react: 19.0.0
  react-native: 0.78.x
Device: iPhone (physical), iOS 26.5.1
JS engine: JSC, old architecture (newArchEnabled=false) — not relevant, the guard runs before the bridge
  • Platform that you're experiencing the issue on:
    • iOS
    • Android
    • iOS but have not tested behavior on Android
    • Android but have not tested behavior on iOS
    • Both
  • react-native-firebase version you're using that has this issue:
    • 19.3.0 (reproduced); the affected code is unchanged on main (26.x)
  • Firebase module(s) you're using that has the issue:
    • Messaging
  • Are you using TypeScript?
    • Y & 5.x


Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions