Which plugins are affected?
Firestore
Which platforms are affected?
Android
Description
Since cloud_firestore 6.8.0, every successful runTransaction() call on Android throws an unhandled MissingPluginException through FlutterError.onError immediately after the transaction completes:
MissingPluginException(No implementation found for method cancel on channel
plugins.flutter.io/firebase_firestore/transaction/<uuid>)
The transaction itself commits correctly — only the stream teardown fails — but because it is reported via FlutterError.reportError, it surfaces in the console on every transaction and is picked up by crash reporters (Sentry/Crashlytics), producing continuous noise.
Cause
This is a race introduced by #18475 ("clean up Android transaction listeners on completion"), released in 6.8.0.
Native side — TransactionStreamHandler.onListen, addOnCompleteListener (android/.../streamhandler/TransactionStreamHandler.java):
mainLooper.post(
() -> {
events.success(map); // {complete: true}
events.endOfStream();
onTransactionCompleteListener.onComplete(transactionId);
});
onComplete → FlutterFirebaseFirestorePlugin.removeTransaction → removeEventListener → eventChannels.remove(identifier) and eventChannel.setStreamHandler(null), which unregisters the message handler for plugins.flutter.io/firebase_firestore/transaction/<id>.
Dart side — MethodChannelFirebaseFirestore.runTransaction (cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart):
return completer.future.whenComplete(snapshotStreamSubscription.cancel);
events.success({complete: true}) completes the completer, but whenComplete runs in a later microtask. By the time the subscription's cancel message is sent, setStreamHandler(null) has already run synchronously in the same main-looper post, so there is no handler left for that channel and EventChannel's cancel path reports a MissingPluginException via its "while de-activating platform stream" error handler.
iOS is unaffected: FLTFirebaseFirestorePlugin keeps the per-transaction event channel registered after completion, so the cancel still finds a handler.
Suggested fixes (either would do)
- Defer the cleanup until the Dart side has cancelled — i.e. run
removeTransaction(transactionId) from TransactionStreamHandler.onCancel rather than from the completion listener, or post it to the back of the main-looper queue after the Dart cancel has been processed.
- Make the Dart teardown tolerant: swallow
MissingPluginException around snapshotStreamSubscription.cancel in runTransaction, since the native listener is expected to be gone once the transaction has terminated.
Reproducing the issue
On Android, with cloud_firestore: 6.8.0:
await Firebase.initializeApp();
await FirebaseFirestore.instance.runTransaction((transaction) async {
transaction.set(
FirebaseFirestore.instance.collection('debug').doc('t1'),
{'updatedAt': DateTime.now().toIso8601String()},
);
});
The transaction succeeds, and the MissingPluginException above is printed immediately afterwards. The removeTransaction/removeEventListener code path responsible does not exist in 6.7.1 or earlier.
Firebase Core version
4.13.0
Flutter Version
3.44.8 (stable), Dart 3.12.2
Relevant Log Output
════════ Exception caught by services library ══════════════════════════════════
The following MissingPluginException was thrown while de-activating platform stream on channel plugins.flutter.io/firebase_firestore/transaction/96932cbd-1367-423d-9baa-1d57c2062060:
MissingPluginException(No implementation found for method cancel on channel plugins.flutter.io/firebase_firestore/transaction/96932cbd-1367-423d-9baa-1d57c2062060)
When the exception was thrown, this was the stack:
#0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:364:7)
<asynchronous suspension>
#1 EventChannel.receiveBroadcastStream.<anonymous closure> (package:flutter/src/services/platform_channel.dart:726:11)
<asynchronous suspension>
════════════════════════════════════════════════════════════════════════════════
Flutter dependencies
cloud_firestore: 6.8.0
cloud_firestore_platform_interface: 8.0.6
firebase_core: 4.13.0
firebase_core_platform_interface: 8.1.0
Additional context and comments
Regression range: not present in 6.7.1, present in 6.8.0. Introduced by #18475.
Which plugins are affected?
Firestore
Which platforms are affected?
Android
Description
Since
cloud_firestore6.8.0, every successfulrunTransaction()call on Android throws an unhandledMissingPluginExceptionthroughFlutterError.onErrorimmediately after the transaction completes:The transaction itself commits correctly — only the stream teardown fails — but because it is reported via
FlutterError.reportError, it surfaces in the console on every transaction and is picked up by crash reporters (Sentry/Crashlytics), producing continuous noise.Cause
This is a race introduced by #18475 ("clean up Android transaction listeners on completion"), released in 6.8.0.
Native side —
TransactionStreamHandler.onListen,addOnCompleteListener(android/.../streamhandler/TransactionStreamHandler.java):onComplete→FlutterFirebaseFirestorePlugin.removeTransaction→removeEventListener→eventChannels.remove(identifier)andeventChannel.setStreamHandler(null), which unregisters the message handler forplugins.flutter.io/firebase_firestore/transaction/<id>.Dart side —
MethodChannelFirebaseFirestore.runTransaction(cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart):events.success({complete: true})completes the completer, butwhenCompleteruns in a later microtask. By the time the subscription'scancelmessage is sent,setStreamHandler(null)has already run synchronously in the same main-looper post, so there is no handler left for that channel andEventChannel's cancel path reports aMissingPluginExceptionvia its "while de-activating platform stream" error handler.iOS is unaffected:
FLTFirebaseFirestorePluginkeeps the per-transaction event channel registered after completion, so thecancelstill finds a handler.Suggested fixes (either would do)
removeTransaction(transactionId)fromTransactionStreamHandler.onCancelrather than from the completion listener, or post it to the back of the main-looper queue after the Dart cancel has been processed.MissingPluginExceptionaroundsnapshotStreamSubscription.cancelinrunTransaction, since the native listener is expected to be gone once the transaction has terminated.Reproducing the issue
On Android, with
cloud_firestore: 6.8.0:The transaction succeeds, and the
MissingPluginExceptionabove is printed immediately afterwards. TheremoveTransaction/removeEventListenercode path responsible does not exist in 6.7.1 or earlier.Firebase Core version
4.13.0
Flutter Version
3.44.8 (stable), Dart 3.12.2
Relevant Log Output
Flutter dependencies
Additional context and comments
Regression range: not present in 6.7.1, present in 6.8.0. Introduced by #18475.