Skip to content

fix: handle exceptions when FlutterEngine is suspended#595

Merged
ttypic merged 1 commit intomainfrom
ECO-5703/fix-errors-because-of-flutter-engine-inactivity
Apr 8, 2026
Merged

fix: handle exceptions when FlutterEngine is suspended#595
ttypic merged 1 commit intomainfrom
ECO-5703/fix-errors-because-of-flutter-engine-inactivity

Conversation

@ttypic
Copy link
Copy Markdown
Contributor

@ttypic ttypic commented Apr 7, 2026

Resolves #594

  • Added exception handling in AblyStreamsChannel to prevent crashes when FlutterEngine is stopped.
  • Improved logging to notify dropped events due to engine inactivity.
  • Added iOS unit tests for regression and normal operation of event sinks.
  • Configured GitHub Actions to run iOS unit tests on macOS.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented crashes when the Flutter engine is not running by safeguarding event emission.
  • Tests

    • Added iOS unit tests to verify stream sink behavior and regression coverage.
  • Chores

    • Configured CocoaPods test spec for iOS unit tests.
    • Added CI workflow for automated iOS unit testing.

@ttypic ttypic requested review from maratal and sacOO7 April 7, 2026 11:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Walkthrough

Wraps event-sink dispatch in AblyStreamsChannel with exception handling to prevent NSException from escaping when the Flutter engine is not running. Adds regression tests exercising the guarded behavior, a CocoaPods test spec, and a GitHub Actions workflow to run iOS unit tests.

Changes

Cohort / File(s) Summary
Event dispatch guard
ios/Classes/AblyStreamsChannel.m
Wraps stream.sink sendOnChannel dispatch in @try/@catch to catch NSException, log a dropped-event message, and suppress exceptions when the Flutter engine is not running.
iOS unit tests
ios/Tests/AblyStreamsChannelTests.m
New test file adding MockThrowingMessenger, CapturingSinkHandler, and two regression tests verifying the sink does not crash when the messenger throws and behaves normally otherwise.
Podspec test target
ios/ably_flutter.podspec
Removes simulator-only VALID_ARCHS tweak and adds a s.test_spec 'Tests' that includes Tests/**/*.{h,m} and links XCTest.
CI: iOS unit test workflow
.github/workflows/ios_unit_tests.yml
New GitHub Actions workflow that sets up macOS simulator, installs Flutter, runs pod lib lint to generate a test workspace, and runs xcodebuild test against the generated scheme for the test spec.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I caught a tumble in a gentle try,

When engines nap, no crash will fly.
With mock messengers I test and sing,
Safe events hop on a guarded string. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR partially addresses issue #594's objectives: it implements exception handling in AblyStreamsChannel's event sink (objective 4) and adds regression tests, but does not implement detachFromEngineForRegistrar: (objective 1) or nil-guards in AblyFlutterStreamHandler (objective 2). Verify with the team whether this PR is intended to fully resolve #594 or is a partial fix; additional work may be needed to address all identified root causes in #594.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding exception handling in AblyStreamsChannel when the FlutterEngine is suspended, which is the core fix in the PR.
Out of Scope Changes check ✅ Passed All changes directly support the stated objective of handling exceptions when the FlutterEngine is suspended: exception handling in AblyStreamsChannel, regression tests, iOS test configuration, and GitHub Actions workflow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ECO-5703/fix-errors-because-of-flutter-engine-inactivity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot temporarily deployed to staging/pull/595/features April 7, 2026 11:32 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/595/dartdoc April 7, 2026 11:33 Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ios/Classes/AblyFlutterStreamHandler.m (1)

132-151: ⚠️ Potential issue | 🔴 Critical

Unreachable code: cancelListening has duplicate conditions making cleanup branches dead code.

All three if/else if conditions on lines 132, 134, and 144 compare against AblyPlatformMethod_onRealtimeConnectionStateChanged. This means:

  • Line 134-143 (channel off:) is unreachable
  • Line 144-151 (channel unsubscribe:) is unreachable

These should check AblyPlatformMethod_onRealtimeChannelStateChanged and AblyPlatformMethod_onRealtimeChannelMessage respectively, mirroring the startListening method structure.

This bug is explicitly called out in linked issue #594 as objective #3: "Fix cancelListening logic so each listener branch is reachable and cleaned up."

🐛 Proposed fix
     if([AblyPlatformMethod_onRealtimeConnectionStateChanged isEqual: eventName]) {
         [[_instanceStore realtimeFrom:handle].connection off: listener];
-    } else if([AblyPlatformMethod_onRealtimeConnectionStateChanged isEqual: eventName]) {
+    } else if([AblyPlatformMethod_onRealtimeChannelStateChanged isEqual: eventName]) {
         // Note: this and all other assert statements in this onCancel method are
         // left as is as there is no way of propagating this error to flutter side
         NSAssert(eventMessage.message, @"event message is missing");
         NSMutableDictionary<NSString *, NSObject *>* eventPayload = eventMessage.message;
         ARTRealtime *const realtime = [_instanceStore realtimeFrom:handle];
         
         NSString *const channelName = (NSString*) eventPayload[TxTransportKeys_channelName];
         ARTRealtimeChannel *const channel  = [realtime.channels get:channelName];
         [channel off: listener];
-    } else if([AblyPlatformMethod_onRealtimeConnectionStateChanged isEqual: eventName]) {
+    } else if([AblyPlatformMethod_onRealtimeChannelMessage isEqual: eventName]) {
         NSAssert(eventMessage.message, @"event message is missing");
         NSMutableDictionary<NSString *, NSObject *>* eventPayload = eventMessage.message;
         ARTRealtime *const realtime = [_instanceStore realtimeFrom:handle];
         
         NSString *const channelName = (NSString*) eventPayload[TxTransportKeys_channelName];
         ARTRealtimeChannel *const channel  = [realtime.channels get:channelName];
         [channel unsubscribe: listener];
     } else if ([AblyPlatformMethod_onRealtimePresenceMessage isEqual: eventName]) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Classes/AblyFlutterStreamHandler.m` around lines 132 - 151, In
cancelListening, the three conditional branches all check
AblyPlatformMethod_onRealtimeConnectionStateChanged, making the channel cleanup
branches unreachable; update the second condition to check
AblyPlatformMethod_onRealtimeChannelStateChanged and the third to check
AblyPlatformMethod_onRealtimeChannelMessage so the branches that call [channel
off: listener] and [channel unsubscribe: listener] execute; keep the existing
eventMessage handling, NSAssert calls, and use the same
_instanceStore/realtime/channel retrieval logic to mirror startListening's
structure and ensure each listener branch is properly cleaned up.
🧹 Nitpick comments (1)
.github/workflows/check.yaml (1)

55-61: Consider using a more stable simulator destination.

The destination name=iPhone 16 may not be available on all macos-latest runner images, depending on the Xcode version installed. This could cause intermittent CI failures.

Consider using a more generic approach:

♻️ Suggested alternatives

Option 1 - Use a device family that's consistently available:

           xcodebuild test \
             -workspace example/ios/Runner.xcworkspace \
             -scheme ably_flutter-Tests \
-            -destination 'platform=iOS Simulator,OS=latest,name=iPhone 16' \
+            -destination 'platform=iOS Simulator,OS=latest,name=iPhone 15' \
             CODE_SIGNING_ALLOWED=NO

Option 2 - Use any iOS Simulator to let Xcode pick:

           xcodebuild test \
             -workspace example/ios/Runner.xcworkspace \
             -scheme ably_flutter-Tests \
-            -destination 'platform=iOS Simulator,OS=latest,name=iPhone 16' \
+            -destination 'generic/platform=iOS Simulator' \
             CODE_SIGNING_ALLOWED=NO
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/check.yaml around lines 55 - 61, The CI step "Run iOS unit
tests" uses xcodebuild test with a fixed destination name ('-destination
"platform=iOS Simulator,OS=latest,name=iPhone 16"') which can be missing on some
macos-latest images; change the -destination argument in that xcodebuild test
invocation to a more stable selector such as a device-family or a generic
simulator (e.g. replace the name=... selector with a consistently available
device like device=iPhone 8 or use a generic selector like just 'platform=iOS
Simulator,OS=latest' / 'platform=iOS Simulator,OS=latest,any' so Xcode can pick
an available simulator), updating the command in the "Run iOS unit tests" step
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/Classes/AblyFlutterStreamHandler.m`:
- Around line 122-124: The catch block in onListenWithArguments: currently only
logs exceptions which can leave the Dart stream hanging; update the `@catch` in
AblyFlutterStreamHandler.m to notify Flutter by sending a FlutterError to the
event sink (e.g. call the instance's event sink with a FlutterError created via
+errorWithCode:message:details: including exception.reason) and then rethrow or
return an error so the failure is not silently swallowed; reference the
onListenWithArguments: method and the class AblyFlutterStreamHandler when making
this change.

---

Outside diff comments:
In `@ios/Classes/AblyFlutterStreamHandler.m`:
- Around line 132-151: In cancelListening, the three conditional branches all
check AblyPlatformMethod_onRealtimeConnectionStateChanged, making the channel
cleanup branches unreachable; update the second condition to check
AblyPlatformMethod_onRealtimeChannelStateChanged and the third to check
AblyPlatformMethod_onRealtimeChannelMessage so the branches that call [channel
off: listener] and [channel unsubscribe: listener] execute; keep the existing
eventMessage handling, NSAssert calls, and use the same
_instanceStore/realtime/channel retrieval logic to mirror startListening's
structure and ensure each listener branch is properly cleaned up.

---

Nitpick comments:
In @.github/workflows/check.yaml:
- Around line 55-61: The CI step "Run iOS unit tests" uses xcodebuild test with
a fixed destination name ('-destination "platform=iOS
Simulator,OS=latest,name=iPhone 16"') which can be missing on some macos-latest
images; change the -destination argument in that xcodebuild test invocation to a
more stable selector such as a device-family or a generic simulator (e.g.
replace the name=... selector with a consistently available device like
device=iPhone 8 or use a generic selector like just 'platform=iOS
Simulator,OS=latest' / 'platform=iOS Simulator,OS=latest,any' so Xcode can pick
an available simulator), updating the command in the "Run iOS unit tests" step
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ea46c1ad-64ef-496b-9826-8352887b3809

📥 Commits

Reviewing files that changed from the base of the PR and between 6a93ac5 and fbe6337.

📒 Files selected for processing (5)
  • .github/workflows/check.yaml
  • ios/Classes/AblyFlutterStreamHandler.m
  • ios/Classes/AblyStreamsChannel.m
  • ios/Tests/AblyStreamsChannelTests.m
  • ios/ably_flutter.podspec

@ttypic ttypic marked this pull request as draft April 7, 2026 12:01
@ttypic ttypic force-pushed the ECO-5703/fix-errors-because-of-flutter-engine-inactivity branch from fbe6337 to 9787ec7 Compare April 7, 2026 14:25
@github-actions github-actions bot temporarily deployed to staging/pull/595/dartdoc April 7, 2026 14:26 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/595/features April 7, 2026 14:26 Inactive
@ttypic ttypic marked this pull request as ready for review April 7, 2026 15:00
Copy link
Copy Markdown
Collaborator

@maratal maratal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (as a quick fix until more info available)

…mission

- Added exception handling in `AblyStreamsChannel` to prevent crashes when FlutterEngine is stopped.
- Improved logging to notify dropped events due to engine inactivity.
- Added iOS unit tests for regression and normal operation of event sinks.
- Configured GitHub Actions to run iOS unit tests on macOS.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ios_unit_tests.yml:
- Around line 22-39: The workflow is using the wrong test scheme name: update
the xcodebuild test invocation in the "Run iOS unit tests" step to use the
scheme generated by the podspec test_spec (change ably_flutter-Unit-Tests to
ably_flutter-Tests) so xcodebuild uses the scheme created by the prior pod lib
lint step (workspace at /tmp/ably_test_build/App.xcworkspace).

In `@ios/Classes/AblyStreamsChannel.m`:
- Around line 112-125: The try block currently wraps both envelope encoding and
sending in stream.sink, causing codec exceptions from _codec
encodeErrorEnvelope: or _codec encodeSuccessEnvelope: to be misreported as
engine inactivity; refactor stream.sink so you first compute the message (handle
FlutterEndOfEventStream, detect FlutterError and call _codec
encodeErrorEnvelope:, otherwise call _codec encodeSuccessEnvelope:) outside the
`@try`, then use a minimal `@try` around only [_messenger sendOnChannel:name
message:message] to catch messenger/engine errors; reference stream.sink,
FlutterEndOfEventStream, _codec encodeErrorEnvelope:/encodeSuccessEnvelope:, and
_messenger sendOnChannel:name message: to locate the changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5dc6a9a1-90c4-45df-bb33-4b3a747c8298

📥 Commits

Reviewing files that changed from the base of the PR and between 9787ec7 and 864e0bb.

📒 Files selected for processing (4)
  • .github/workflows/ios_unit_tests.yml
  • ios/Classes/AblyStreamsChannel.m
  • ios/Tests/AblyStreamsChannelTests.m
  • ios/ably_flutter.podspec
🚧 Files skipped from review as they are similar to previous changes (1)
  • ios/ably_flutter.podspec

@ttypic ttypic merged commit 8be5c93 into main Apr 8, 2026
16 of 22 checks passed
@ttypic ttypic deleted the ECO-5703/fix-errors-because-of-flutter-engine-inactivity branch April 8, 2026 10:06
@ttypic ttypic mentioned this pull request Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants