Repository navigation
Description
Steps to reproduce
Needs the Play Billing library to invoke the response listener of a single
async call twice — observed in production via its internal
watchdog/timeout path (zzaf.run → zzan.run), ~30 s after the app was
backgrounded with a queryProductDetailsAsync call still in flight:
- App calls
InAppPurchase.instance.queryProductDetails(...)(or any other
async billing method). - App is backgrounded while the query is in flight.
- Play Billing's internal watchdog fires a (timeout/error) response, AND the
real response also arrives — the SAMEProductDetailsResponseListeneris
invoked twice. - The second invocation completes the pigeon callback a second time →
reply.reply()throwsIllegalStateException: Reply already submittedon
the main thread → uncaught → process death (in background, so users see
nothing; crash reporting and Play vitals do).
Expected results
A duplicate listener invocation is ignored; the first completion wins.
Actual results
java.lang.IllegalStateException: Reply already submitted
DartMessenger$Reply.reply ← BasicMessageChannel reply (pigeon)
Messages$InAppPurchaseApi.success
← MethodCallHandlerImpl.lambda$queryProductDetailsAsync (2nd completion)
com.android.billingclient.api.zzan.run ← zzaf.run # Play Billing watchdog
Analysis
A pigeon reply may be submitted exactly once. Upstream clearly knows this bug
class — startConnection carries its own alreadyFinished guard against a
double onBillingSetupFinished — but none of the other async methods have
one (verified on 0.5.2 and main, 2026-08-26): queryProductDetailsAsync,
queryPurchasesAsync, consumeAsync, acknowledgePurchase,
getBillingConfigAsync, showInAppMessages,
isAlternativeBillingOnlyAvailableAsync,
createAlternativeBillingOnlyReportingDetailsAsync,
showAlternativeBillingOnlyInformationDialog.
Prior reports of this crash class: #103432 (open since 2022), #134967 (closed
as its duplicate). For comparison, RevenueCat's Android SDK guards every
billing listener with an AtomicBoolean.getAndSet(true) single-shot for the
same reason.
Proposed fix
A small replyOnce(methodName, callback) wrapper
(AtomicBoolean.getAndSet(true); first completion wins, duplicates dropped
with a Log.d) applied to the pigeon callback in all 9 async methods, keeping
the method bodies otherwise unchanged. startConnection keeps its existing
guard. A PR implementing this (plus two JUnit regression tests that fire a
captured listener twice and assert exactly one pigeon completion holding the
first result) is attached.
Environment
in_app_purchase_android: crash first captured in production 2026-05-14 on
0.4.0+8; guard hole code-verified present on0.5.2(latest release) and
mainas of 2026-08-26.- Flutter: 3.44.4 (stable), Dart 3.12.2
flutter doctor -v
[✓] Flutter (Channel stable, 3.44.4, on Debian GNU/Linux 12 (bookworm) 6.1.0-37-amd64, locale en_US.UTF-8) [216ms]
• Flutter version 3.44.4 on channel stable at /home/elinorapricot/devtools/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision ad70ec4617 (9 weeks ago), 2026-06-24 11:07:06 -0700
• Engine revision a10d8ac38d
• Dart version 3.12.2
• DevTools version 2.57.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [13.6s]
• Android SDK at /home/elinorapricot/Android/Sdk/
• Emulator version 36.5.11.0 (build_id 15261927) (CL:N/A)
• Platform android-36.1, build-tools 36.1.0
• Java binary at: /home/elinorapricot/Tools/android-studio/jbr/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 17.0.10+0-17.0.10b1087.21-11609105)
• All Android licenses accepted.
[✓] Chrome - develop for the web [72ms]
• CHROME_EXECUTABLE = /usr/bin/chromium
[✓] Linux toolchain - develop for Linux desktop [1,879ms]
• Debian clang version 14.0.6
• cmake version 3.25.1
• ninja version 1.11.1
• pkg-config version 1.8.1
• GL_EXT_framebuffer_blit: no
• GL_EXT_texture_format_BGRA8888: no
[✓] Connected device (3 available) [1,932ms]
• sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64 • Android 16 (API 36) (emulator)
• Linux (desktop) • linux • linux-x64 • Debian GNU/Linux 12 (bookworm) 6.1.0-37-amd64
• Chrome (web) • chrome • web-javascript • Chromium 140.0.7339.127 built on Debian GNU/Linux 12 (bookworm)
[✓] Network resources [1,087ms]
• All expected network resources are available.
• No issues found!
Activity
puneetkukreja98 commentedon Aug 27, 2026
@eszter-varga Closing this as duplicate of #103432, feel free to comment if you disagree, I will re-open it.
Thanks.
github-actions commentedon Sep 10, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.[in_app_purchase_android] Guard async billing callbacks against dupli…