Skip to content

[cloud_firestore][android] MissingPluginException on transaction channel cancel after every runTransaction since 6.8.0 (#18475) #18546

Description

@KestasVenslauskas

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);
    });

onCompleteFlutterFirebaseFirestorePlugin.removeTransactionremoveEventListenereventChannels.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)

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions