Description
When an iOS app is backgrounded and the FlutterEngine is suspended/detached by the OS, the native Ably SDK continues to receive connection state change events. AblyFlutterStreamHandler attempts to emit these events through the FlutterBinaryMessengerRelay, which calls [FlutterEngine sendOnChannel:message:binaryReply:] — but the engine is no longer running, causing a fatal NSInternalInconsistencyException.
This is a fatal, unhandled crash affecting production users.
Stack Trace
NSInternalInconsistencyException: Sending a message before the FlutterEngine has been run.
at -[FlutterEngine sendOnChannel:message:binaryReply:] (FlutterEngine.mm:1305)
at -[FlutterBinaryMessengerRelay sendOnChannel:message:] (FlutterBinaryMessengerRelay.mm:24)
at __69-[AblyStreamsChannel listenForCall:withKey:usingCallback:andFactory:]_block_invoke (AblyStreamsChannel.m)
at __51-[AblyFlutterStreamHandler startListening:emitter:]_block_invoke (AblyFlutterStreamHandler.m:60)
Root Cause
Two issues in the native iOS plugin code:
1. Missing detachFromEngineForRegistrar: in AblyFlutter.m
AblyFlutter.m does not implement detachFromEngineForRegistrar:. When iOS tears down the Flutter engine (e.g., during background suspension), native Ably listeners remain active with stale references to the Flutter channel. This is the primary cause — the plugin has no cleanup path for engine detachment.
2. Unguarded emitter() calls in AblyFlutterStreamHandler.m
AblyFlutterStreamHandler.m calls emitter() (the FlutterEventSink) directly without checking if the event sink or engine is still valid. Every call site (connection state changes, channel state changes, messages, presence messages, errors) is unguarded.
Bonus: cancelListening logic bug
In AblyFlutterStreamHandler.m, the cancelListening method appears to have a copy-paste bug — the first three conditions all check AblyPlatformMethod_onRealtimeConnectionStateChanged, which makes the channel state and presence listener cleanup branches unreachable.
Expected Behavior
The plugin should:
- Implement
detachFromEngineForRegistrar: to nil out channels and event sinks when the engine is detached
- Nil-check
emitter before every invocation in AblyFlutterStreamHandler.m
- Not crash when the app is backgrounded and the native SDK receives state change events
Context
This is a well-known class of Flutter plugin bug. The Flutter team's position is that plugins are responsible for handling engine teardown:
Environment
- ably_flutter: 1.2.43
- Flutter: 3.41.6
- iOS: 26.4
- Device: iPhone 18,2 (arm64)
Impact
681 occurrences across 606 users in our production app. The crash occurs when the app has been backgrounded for an extended period (hours).
Workaround
We are mitigating this app-side by calling _pub.connection.close() when the app enters AppLifecycleState.paused and _pub.connection.connect() on resume, to prevent native state-change events from firing while the engine may be detached.
┆Issue is synchronized with this Jira Task by Unito
Description
When an iOS app is backgrounded and the
FlutterEngineis suspended/detached by the OS, the native Ably SDK continues to receive connection state change events.AblyFlutterStreamHandlerattempts to emit these events through theFlutterBinaryMessengerRelay, which calls[FlutterEngine sendOnChannel:message:binaryReply:]— but the engine is no longer running, causing a fatalNSInternalInconsistencyException.This is a fatal, unhandled crash affecting production users.
Stack Trace
Root Cause
Two issues in the native iOS plugin code:
1. Missing
detachFromEngineForRegistrar:inAblyFlutter.mAblyFlutter.mdoes not implementdetachFromEngineForRegistrar:. When iOS tears down the Flutter engine (e.g., during background suspension), native Ably listeners remain active with stale references to the Flutter channel. This is the primary cause — the plugin has no cleanup path for engine detachment.2. Unguarded
emitter()calls inAblyFlutterStreamHandler.mAblyFlutterStreamHandler.mcallsemitter()(theFlutterEventSink) directly without checking if the event sink or engine is still valid. Every call site (connection state changes, channel state changes, messages, presence messages, errors) is unguarded.Bonus:
cancelListeninglogic bugIn
AblyFlutterStreamHandler.m, thecancelListeningmethod appears to have a copy-paste bug — the first three conditions all checkAblyPlatformMethod_onRealtimeConnectionStateChanged, which makes the channel state and presence listener cleanup branches unreachable.Expected Behavior
The plugin should:
detachFromEngineForRegistrar:to nil out channels and event sinks when the engine is detachedemitterbefore every invocation inAblyFlutterStreamHandler.mContext
This is a well-known class of Flutter plugin bug. The Flutter team's position is that plugins are responsible for handling engine teardown:
Fatal Exception: NSInternalInconsistencyException Sending a message before the FlutterEngine has been run.flutter/flutter#96901Environment
Impact
681 occurrences across 606 users in our production app. The crash occurs when the app has been backgrounded for an extended period (hours).
Workaround
We are mitigating this app-side by calling
_pub.connection.close()when the app entersAppLifecycleState.pausedand_pub.connection.connect()on resume, to prevent native state-change events from firing while the engine may be detached.┆Issue is synchronized with this Jira Task by Unito