You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PushNotificationsManager.handleNotification(_:userInteraction:completionHandler:) can return without ever invoking the OS-supplied fetchCompletionHandler. For a content-available (silent/background) push, the app then never signals iOS that it finished its background work — wasting the background assertion and, over repeated occurrences, leading iOS to throttle the app's future background/silent-push wakeups and background-fetch budget.
Pre-existing — not introduced by any recent change. Surfaced while reviewing #25643 (UIScene adoption), which makes windowless background launches a routinely-exercised path but does not touch this dispatch loop.
Root Cause
The OS entry point hands handleNotification a non-optional escaping handler (application(_:didReceiveRemoteNotification:fetchCompletionHandler:)). But handleNotification only calls it inside the four sub-handlers' success paths, with no terminal call when nothing handles the push — plus an early return on the badge path:
// Badge: Reset
guardlet type = userInfo.string(forKey:Notification.typeKey), type !=Notification.badgeResetValue else{return // ← completionHandler never called
}lethandlers=[
handleSupportNotification,
handleAuthenticationNotification,
handleInactiveNotification,
handleBackgroundNotification
]forhandlerin handlers {ifhandler(userInfo, userInteraction, completionHandler){break}}
// ← if every handler returned false, completionHandler is never called
carries no note_id → handleBackgroundNotification returns false (it requires Notification.identifierKey).
All four decline; completionHandler is never called. The badge-reset early-return at L208 is the same drop, though a badge-reset push isn't usually a content-available push the OS waits on.
In practice, real WP.com notification pushes (comment / like / follow / …) all carry note_id, so they land in handleBackgroundNotification and do call the handler. The drop therefore needs a malformed or future/unknown payload shape — latent and low-frequency, but unbounded by anything in the client.
Impact
A wasted ~30s background assertion per occurrence (the OS waits out the watchdog, then suspends the app).
iOS meters completion-handler reliability; repeated drops contribute to throttling of future silent-push delivery and background-fetch budget — i.e. degraded background responsiveness (share-extension upload completion, silent syncs).
No crash, no data loss — a robustness gap.
Fix
Guarantee the handler is called exactly once on every path:
guardlet type = userInfo.string(forKey:Notification.typeKey), type !=Notification.badgeResetValue else{completionHandler?(.noData)return}forhandlerin handlers {ifhandler(userInfo, userInteraction, completionHandler){return // a handler took it (and already called completionHandler)
}}completionHandler?(.noData) // nothing handled it — still fulfil the OS contract
The return-on-handled keeps it single-call (no double invocation when a handler already called it).
Notes
Confirmed pre-existing: the dispatch loop and badge-reset return are identical on trunk and predate Adopt the UIScene life cycle #25643. Adopt the UIScene life cycle #25643's only change to this file (widening handleInactiveNotification to accept .background && userInteraction) shrinks the set of dropped pushes — it neither introduces nor worsens this.
Summary
PushNotificationsManager.handleNotification(_:userInteraction:completionHandler:)can return without ever invoking the OS-suppliedfetchCompletionHandler. For acontent-available(silent/background) push, the app then never signals iOS that it finished its background work — wasting the background assertion and, over repeated occurrences, leading iOS to throttle the app's future background/silent-push wakeups and background-fetch budget.Pre-existing — not introduced by any recent change. Surfaced while reviewing #25643 (UIScene adoption), which makes windowless background launches a routinely-exercised path but does not touch this dispatch loop.
Root Cause
The OS entry point hands
handleNotificationa non-optional escaping handler (application(_:didReceiveRemoteNotification:fetchCompletionHandler:)). ButhandleNotificationonly calls it inside the four sub-handlers' success paths, with no terminal call when nothing handles the push — plus an earlyreturnon the badge path:WordPress-iOS/WordPress/Classes/Utility/Notifications/PushNotificationsManager.swift
Lines 208 to 224 in f039b67
Trigger
A remote push delivered in the
.backgroundstate withuserInteraction == falsethat:type(so it survives the badge-resetguard),handleSupportNotificationreturnsfalse,handleAuthenticationNotificationreturnsfalse,handleInactiveNotificationreturnsfalse(.background && !userInteraction),note_id→handleBackgroundNotificationreturnsfalse(it requiresNotification.identifierKey).All four decline;
completionHandleris never called. The badge-reset early-return at L208 is the same drop, though a badge-reset push isn't usually acontent-availablepush the OS waits on.In practice, real WP.com notification pushes (comment / like / follow / …) all carry
note_id, so they land inhandleBackgroundNotificationand do call the handler. The drop therefore needs a malformed or future/unknown payload shape — latent and low-frequency, but unbounded by anything in the client.Impact
No crash, no data loss — a robustness gap.
Fix
Guarantee the handler is called exactly once on every path:
The
return-on-handled keeps it single-call (no double invocation when a handler already called it).Notes
returnare identical ontrunkand predate Adopt the UIScene life cycle #25643. Adopt the UIScene life cycle #25643's only change to this file (wideninghandleInactiveNotificationto accept.background && userInteraction) shrinks the set of dropped pushes — it neither introduces nor worsens this.