Skip to content

[in_app_purchase_android] IllegalStateException "Reply already submitted": Play Billing can invoke a response listener twice for one call; only startConnection is guarded #191832

Closed as duplicate
flutter/packages
#12640

Description

@eszter-varga

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:

  1. App calls InAppPurchase.instance.queryProductDetails(...) (or any other
    async billing method).
  2. App is backgrounded while the query is in flight.
  3. Play Billing's internal watchdog fires a (timeout/error) response, AND the
    real response also arrives — the SAME ProductDetailsResponseListener is
    invoked twice.
  4. The second invocation completes the pigeon callback a second time →
    reply.reply() throws IllegalStateException: Reply already submitted on
    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 on 0.5.2 (latest release) and
    main as 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

  1. puneetkukreja98 commented on Aug 27, 2026

    @puneetkukreja98
    Contributor

    @eszter-varga Closing this as duplicate of #103432, feel free to comment if you disagree, I will re-open it.

    Thanks.

  2. added
    r: duplicateIssue is closed as a duplicate of an existing issue
    and removed
    in triagePresently being triaged by the triage team
    on Aug 27, 2026
  3. github-actions commented on Sep 10, 2026

    @github-actions

    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 -v and a minimal reproduction of the issue.

  4. locked as resolved and limited conversation to collaborators on Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    r: duplicateIssue is closed as a duplicate of an existing issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions