Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.

fix(ios): resolve fetchProducts 30-second timeout#19

Merged
hyochan merged 3 commits into
mainfrom
fix/ios-fetch-products-timeout
Mar 9, 2026
Merged

fix(ios): resolve fetchProducts 30-second timeout#19
hyochan merged 3 commits into
mainfrom
fix/ios-fetch-products-timeout

Conversation

@hyochan

@hyochan hyochan commented Mar 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Remove DispatchSemaphore blocking pattern in iOS fetchProducts() that caused thread starvation deadlocks
  • Replace with async Task + signal emission pattern (matching requestPurchase and other iOS methods)
  • Update GDScript wrapper to await products_fetched signal on iOS

Root Cause

The iOS fetchProducts() used a DispatchSemaphore to bridge async StoreKit 2 calls to synchronous GDExtension returns. This blocked Godot's main thread, preventing the async Task from executing, causing a deadlock that always resulted in a 30-second timeout.

Changes

iOS (ios-gdextension/)

  • Remove DispatchSemaphore and 30-second timeout logic
  • Return {"status": "pending"} immediately (like requestPurchase)
  • Emit results via products_fetched signal asynchronously

GDScript (addons/godot-iap/)

  • iOS path: call native fetchProducts() then await products_fetched signal
  • Android path: unchanged (synchronous return)

Test plan

  • iOS: fetch_products() returns products without timeout
  • Android: fetch_products() continues to work as before
  • Example project runs in Godot editor

Closes #15

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements

    • iOS product fetching is now fully asynchronous, improving responsiveness; Android behavior is unchanged.
    • The native call now emits results via the existing products-fetched signal while returning immediately.
  • Tests

    • Test flow updated to await product-fetch operations so mock tests reflect asynchronous behavior.
  • Documentation

    • Clarified iOS-specific await behavior for product fetching.

hyochan and others added 2 commits March 9, 2026 12:20
Replace synchronous semaphore-based blocking pattern with async
Task + signal emission, matching the pattern used by requestPurchase
and other iOS methods. The semaphore was causing thread starvation
deadlocks, resulting in 30-second timeouts.

Closes #15

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update GDScript wrapper to use async signal-based flow for iOS
fetchProducts instead of expecting synchronous return. Android
remains unchanged (synchronous).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical deadlock issue in the iOS implementation of fetchProducts() that caused a consistent 30-second timeout. The previous synchronous blocking mechanism using DispatchSemaphore on the main thread has been replaced with a modern asynchronous Task and signal emission pattern. This change significantly improves the responsiveness and reliability of product fetching on iOS, while maintaining existing functionality for Android.

Highlights

  • Timeout Resolution: Resolved a 30-second timeout in iOS fetchProducts() by eliminating a DispatchSemaphore blocking pattern that caused thread starvation deadlocks.
  • Asynchronous Pattern Adoption: Implemented an asynchronous Task and signal emission pattern for iOS fetchProducts(), aligning its behavior with other asynchronous iOS methods like requestPurchase.
  • GDScript Wrapper Update: Updated the GDScript wrapper to await the products_fetched signal specifically for iOS, ensuring proper asynchronous handling and integration with the new native implementation.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • addons/godot-iap/godot_iap.gd
    • Modified fetch_products to await the result from the native plugin.
    • Refactored _fetch_products_raw to differentiate between Android's synchronous return and iOS's new asynchronous signal-based approach, awaiting the products_fetched signal for iOS.
  • ios-gdextension/Sources/GodotIap/GodotIap.swift
    • Removed the DispatchSemaphore and the 30-second timeout logic that caused thread starvation.
    • Updated fetchProducts to return {"status": "pending"} immediately and emit the actual product fetching results asynchronously via the products_fetched signal.
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai

coderabbitai Bot commented Mar 9, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b803db7e-1514-4b78-9301-515dcc8d95a1

📥 Commits

Reviewing files that changed from the base of the PR and between 9cd549b and 9005c03.

📒 Files selected for processing (2)
  • Example/tests/test_godot_iap.gd
  • addons/godot-iap/godot_iap.gd

📝 Walkthrough

Walkthrough

Converts iOS product fetching to an asynchronous, signal-driven flow: the native iOS plugin returns immediately with a pending status and emits a products_fetched signal when results arrive. The Godot layer awaits that signal and returns a product/error dictionary. Android keeps the original synchronous path.

Changes

Cohort / File(s) Summary
GDScript async coordination
addons/godot-iap/godot_iap.gd
fetch_products now awaits _fetch_products_raw. _fetch_products_raw retains Android synchronous branch; iOS branch calls native fetchProducts then awaits the products_fetched signal, builds/returns { "products", "error" }. Documentation note added about iOS await behavior.
Swift plugin non-blocking
ios-gdextension/Sources/GodotIap/GodotIap.swift
Removed semaphore/blocking wait in fetchProducts. The function now returns immediately with a pending status; results are delivered asynchronously via the existing productsFetched signal.
Tests adjusted for async
Example/tests/test_godot_iap.gd
Tests updated to await test_fetch_products_mock() and to await GodotIapPlugin.fetch_products(request) in the mock test flow to match the new async behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant GodotIAP
    participant SwiftPlugin
    participant AppStore

    rect rgba(200,150,100,0.5)
    Note over Client,AppStore: Previous Behavior (Synchronous - Blocking)
    Client->>GodotIAP: fetch_products(request)
    GodotIAP->>SwiftPlugin: fetchProducts(json)
    SwiftPlugin->>SwiftPlugin: create semaphore / wait
    SwiftPlugin->>AppStore: async query products
    AppStore-->>SwiftPlugin: products result
    SwiftPlugin->>SwiftPlugin: build JSON response
    SwiftPlugin-->>GodotIAP: return JSON string (blocking)
    GodotIAP-->>Client: return result
    end

    rect rgba(100,200,150,0.5)
    Note over Client,AppStore: New Behavior (Asynchronous - Signal-based)
    Client->>GodotIAP: fetch_products(request)
    GodotIAP->>GodotIAP: await _fetch_products_raw
    GodotIAP->>SwiftPlugin: fetchProducts(json)
    SwiftPlugin-->>GodotIAP: return pending status (immediate)
    SwiftPlugin->>AppStore: async query products (non-blocking)
    AppStore-->>SwiftPlugin: products result
    SwiftPlugin-->>GodotIAP: emit productsFetched signal
    GodotIAP->>GodotIAP: await signal received
    GodotIAP->>GodotIAP: build and return products/error dictionary
    GodotIAP-->>Client: return result
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hopped from block to signal light,

I wait no more and dance at night.
Swift emits, Godot hears the call,
Products come — no timeout sprawl.
Hooray, async delights for all! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing an iOS fetchProducts timeout issue by removing a blocking synchronous pattern.
Linked Issues check ✅ Passed The code changes directly address the core problem in issue #15: removing the synchronous DispatchSemaphore blocking pattern that caused the 30-second timeout and replacing it with an async signal-based pattern.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the iOS fetchProducts timeout issue; no extraneous modifications detected in the Godot GDScript wrapper or Swift iOS extension.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/ios-fetch-products-timeout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hyochan hyochan mentioned this pull request Mar 9, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively resolves the 30-second timeout issue on iOS for fetchProducts by replacing the blocking DispatchSemaphore with a modern asynchronous Task-based approach that emits a signal. While this improves thread safety and responsiveness, a security concern was identified regarding the logging of sensitive information in the GDScript wrapper due to signal pollution. Additionally, this introduces a breaking API change and appears to break existing unit tests which call fetch_products synchronously. Please review the detailed comments for specific feedback.

func fetch_products(request: Types.ProductRequest) -> Array:
print("[GodotIap] fetch_products called")
var result = _fetch_products_raw(request.to_dict())
var result = await _fetch_products_raw(request.to_dict())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This await makes fetch_products and _fetch_products_raw asynchronous. This breaking change appears to break the unit tests in Example/tests/test_godot_iap.gd. The test test_fetch_products_mock calls fetch_products synchronously, which will now cause the test to fail because it receives a GDScriptFunctionState object instead of an Array. The tests need to be updated to use await when calling fetch_products to work with this change.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9005c03. Updated the test to use await for fetch_products.

Comment thread addons/godot-iap/godot_iap.gd Outdated
_native_plugin.fetchProducts(request_json)
# Await the signal from Swift native plugin
var signal_result: Dictionary = await products_fetched
print("[GodotIap] fetchProducts signal result: ", signal_result)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The code logs the result of the products_fetched signal. However, this signal is shared across many different operations in the native iOS plugin, including sensitive ones like verifyPurchase, getReceiptDataIOS, and getTransactionJwsIOS. If any of these sensitive operations complete while fetch_products is awaiting the signal, their results (which can contain sensitive data like receipt data, JWS tokens, or purchase tokens) will be captured and printed to the application logs. It is recommended to remove this print statement or sanitize the output to prevent leaking sensitive IAP data in production logs.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Removed the signal result print statement in 9005c03 to prevent sensitive data exposure.

Comment thread addons/godot-iap/godot_iap.gd Outdated
## Fetch products from the store
## @param request: Types.ProductRequest object
## Returns Array of typed product objects (Types.ProductAndroid or Types.ProductIOS)
## Note: On iOS this awaits the products_fetched signal internally

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The docstring could be clearer about the implications of this change. Since fetch_products now uses await, it has become an asynchronous function and must be awaited by any caller. This is a significant breaking change to the API. To avoid confusion for developers using this plugin, I recommend making the asynchronous nature more explicit in the documentation.

For example:

## Note: This function is asynchronous and must be called with 'await'.
## On iOS, it awaits the 'products_fetched' signal internally.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9005c03. Updated docstring to explicitly state that the function is async and must be called with await.

@hyochan hyochan added 📱 iOS Related to iOS 🛠 bugfix All kinds of bug fixes labels Mar 9, 2026
- Improve docstring to clarify async/await requirement
- Remove signal result logging to prevent sensitive data exposure
- Update test to use await for fetch_products

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@hyochan hyochan merged commit 495ed1c into main Mar 9, 2026
5 of 6 checks passed
@hyochan hyochan deleted the fix/ios-fetch-products-timeout branch March 9, 2026 08:57
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

🛠 bugfix All kinds of bug fixes 📱 iOS Related to iOS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fetch products timeout

1 participant