Skip to content

[Bug] godot-iap request_purchase can fail without purchase_error #272

Description

@hyochan

Library

godot-iap

Library version

godot-iap 3.0.1 (also present on main at 77f91d963c17629b9b0a3c167cf47497ae3a621b)

Affected platform

Android and iOS

Store

Google Play Store and App Store

Environment

Godot 4.7.x, GDScript addon API

What happened?

The public request_purchase() contract tells callers to rely on purchase_updated / purchase_error, not the return value. However, several terminal failure paths return null without emitting purchase_error:

  • both requestPurchase and requestSubscription are present;
  • neither request is present;
  • the platform payload is not a dictionary;
  • the requested type does not match the request kind;
  • the Android Google payload is invalid;
  • the iOS Apple payload is invalid or has an empty SKU;
  • the platform is unsupported;
  • the native response is malformed JSON.

The relevant implementation and its event-based API documentation are here:

An event-driven purchase screen that enters a busy state before calling request_purchase() can therefore remain stuck forever: it receives neither purchase_updated nor purchase_error, and the documented return value is intentionally not the outcome channel.

Expected behavior

After request_purchase() is invoked, every terminal failure should emit one normalized purchase_error payload. Synchronous validation, unsupported-platform, empty native response, native failure response, and parse failure paths should follow the same event contract.

Alternatively, the public API could return a documented dispatch result, but the current guidance explicitly says not to rely on the return value. Under that contract, exactly one terminal event is the least surprising behavior.

Reproduction steps

  1. Connect handlers to purchase_updated and purchase_error.
  2. Construct props containing both requestPurchase and requestSubscription (or use an empty iOS SKU).
  3. Call await request_purchase(props).
  4. Observe that it returns null.
  5. Observe that neither signal is emitted, so an event-driven caller has no terminal notification.

The malformed-native-response path has the same behavior: _request_purchase_raw() returns { success: false, error: "Failed to parse response" }, then the public wrapper returns null without emitting purchase_error.

Suggested regression coverage

  • Each synchronous validation failure emits exactly one purchase_error.
  • Empty and malformed native responses emit exactly one purchase_error.
  • A native { success: false } result remains exactly-once (no duplicate error event).
  • Successful dispatch still completes through the existing purchase events.

Logs or stack trace

request_purchase(...) -> null
purchase_updated emissions -> 0
purchase_error emissions -> 0

Additional context

Normalizing these paths upstream prevents every Godot consumer from adding its own purchase-state watchdog and duplicate error adapter.

Metadata

Metadata

Assignees

No one assigned

    Labels

    cross-platformCross-platform (both Android & iOS)godot-iapgodot-iap library🐛 bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions