Skip to content

iOS crash: NSInternalInconsistencyException when FlutterEngine is detached (backgrounded app) #594

@owenmoon1

Description

@owenmoon1

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:

  1. Implement detachFromEngineForRegistrar: to nil out channels and event sinks when the engine is detached
  2. Nil-check emitter before every invocation in AblyFlutterStreamHandler.m
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions