diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java index 5b21de64a1dd..e162e6530a52 100644 --- a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java @@ -635,6 +635,8 @@ public void transactionGet( DocumentSnapshot.ServerTimestampBehavior.NONE)); } catch (Exception e) { ExceptionConverter.sendErrorToFlutter(result, e); + } catch (Throwable t) { + ExceptionConverter.sendErrorToFlutter(result, new Exception(t)); } }); } diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/TransactionStreamHandler.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/TransactionStreamHandler.java index 1344b2ddb4ca..6774cb5bf061 100644 --- a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/TransactionStreamHandler.java +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/TransactionStreamHandler.java @@ -87,11 +87,15 @@ public void onListen(Object arguments, EventSink events) { try { if (!semaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS)) { - return FlutterFirebaseFirestoreTransactionResult.failed( + // Throw a non-FirebaseFirestoreException so the Android SDK + // does not retry or commit. Returning a failed result object + // makes apply() succeed, which commits and then crashes a later + // transaction.get (see #18666). + throw new RuntimeException( new FirebaseFirestoreException("timed out", Code.DEADLINE_EXCEEDED)); } } catch (InterruptedException e) { - return FlutterFirebaseFirestoreTransactionResult.failed( + throw new RuntimeException( new FirebaseFirestoreException("interrupted", Code.DEADLINE_EXCEEDED)); } diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/transaction_e2e.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/transaction_e2e.dart index d0fe0e78b66f..64a88f0da45f 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/transaction_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/transaction_e2e.dart @@ -302,6 +302,34 @@ void runTransactionTests() { skip: kIsWeb || defaultTargetPlatform == TargetPlatform.windows, ); + test( + 'should not crash when a get is issued after the transaction timeout', + () async { + final DocumentReference> first = + await initializeTest('timeout-second-get-1'); + final DocumentReference> second = + await initializeTest('timeout-second-get-2'); + await first.set({'v': 1}); + await second.set({'v': 2}); + + await expectLater( + firestore.runTransaction( + (Transaction transaction) async { + await transaction.get(first); + await Future.delayed(const Duration(seconds: 2)); + await transaction.get(second); + }, + timeout: const Duration(seconds: 1), + ), + throwsA( + isA() + .having((e) => e.code, 'code', 'deadline-exceeded'), + ), + ); + }, + skip: kIsWeb || defaultTargetPlatform != TargetPlatform.android, + ); + test('should throw with exception', () async { try { await firestore.runTransaction((Transaction transaction) async {