Skip to content

HS-823973: Defer ClientIO init() to prevent JNI crash on Android release mode#321

Open
claudear wants to merge 2 commits into
mainfrom
fix/HS-823973-defer-client-init
Open

HS-823973: Defer ClientIO init() to prevent JNI crash on Android release mode#321
claudear wants to merge 2 commits into
mainfrom
fix/HS-823973-defer-client-init

Conversation

@claudear

Copy link
Copy Markdown

Summary

  • Remove eager init() call from ClientIO constructor that caused "No JNI instance is available" errors on Android in release mode
  • The constructor was calling init() which triggers getApplicationDocumentsDirectory() before the Flutter engine/JNI bridge is fully ready
  • The call() method already has lazy initialization logic (lines 508-513), so the eager call was unnecessary
  • Added test to verify init() is not called during construction

Test plan

  • Verify existing tests pass (CI)
  • Test Flutter app on Android in release mode — Client() constructor should no longer throw JNI errors
  • Verify API calls still work correctly (lazy init triggers on first call())

🤖 Generated with Claude Code

…Android release mode

The constructor was eagerly calling init(), which triggers
getApplicationDocumentsDirectory() before the Flutter engine/JNI bridge
is fully initialized. This causes "No JNI instance is available" errors
on Android in release mode. The call() method already has lazy init
logic, so removing the eager init() call is safe.

Fixes HS-823973

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes an Android release-mode crash by removing the eager init() call from the ClientIO constructor, deferring cookie-jar and platform-channel setup to the first call(), which already has a lazy-init guard with a polling loop.

  • lib/src/client_io.dart: One-line removal of init() from the constructor; the lazy-init path in call() (lines 508\u2013513) handles all subsequent requests correctly.
  • test/src/client_io_test.dart: New test file with two cases \u2014 one verifying the constructor leaves both initProgress and initialized false, and one exercising init() directly.

Confidence Score: 4/5

Safe for typical API-call flows; the webAuth() OAuth path can crash with an uninitialized cookie jar if a concurrent call() is mid-initialization when the OAuth callback fires.

The call() lazy-init path is correctly guarded, but webAuth() uses a bare await init() that returns early if init is already in progress, leaving _cookieJar uninitialized before saveFromResponse is called.

lib/src/client_io.dart — specifically the webAuth() method's initialization guard

Important Files Changed

Filename Overview
lib/src/client_io.dart Removes eager init() from constructor to fix Android JNI crash; call() lazy-init path is safe, but webAuth() lacks the same wait loop and can crash with LateInitializationError if init is concurrently in progress
test/src/client_io_test.dart New test file verifying constructor no longer eagerly calls init(); second test invokes init() directly rather than through call(), so the actual lazy-init guard in call() remains untested

Comments Outside Diff (1)

  1. lib/src/client_io.dart, line 495-496 (link)

    P1 webAuth() is missing the wait-loop that call() uses to handle in-progress initialization. If a call() starts init() (setting _initProgress = true) and webAuth()'s OAuth callback fires before that init completes, webAuth() calls await init() — which returns immediately at the if (_initProgress) return; guard — and then immediately accesses _cookieJar which is still uninitialized, producing a LateInitializationError at runtime. The call() method handles this with a polling loop; webAuth() should do the same.

Reviews (2): Last reviewed commit: "fix: use system temp directory in client..." | Re-trigger Greptile

Comment on lines +37 to +50
endPoint: 'https://cloud.appwrite.io/v1',
selfSigned: false,
);

// Before any call, client should not be initialized
expect(client.initialized, isFalse);

// Trigger initialization by calling init() directly
await client.init();

// After init, client should be initialized
expect(client.initialized, isTrue);
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Test title doesn't match what's actually tested

The test is named 'init() should be called lazily on first API call', but it never exercises the call() code path — it invokes client.init() directly. This means the guard logic in call() (lines 508-513 of client_io.dart) that triggers lazy init is never exercised by any test in this file. If someone accidentally removed or broke that guard, these tests would still pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claudear

Copy link
Copy Markdown
Author

Fix Confidence: 90/100

High confidence because: (1) the stack trace directly points to init() being called in the constructor triggering getApplicationDocumentsDirectory() before JNI is ready, (2) the call() method already has robust lazy initialization logic (polling loop + fallback init), (3) webAuth() also calls await init() before accessing _cookieJar, so all code paths are covered, (4) the fix is minimal (1 line removal), and (5) all CI tests pass. Slight uncertainty only because I can't test on an actual Android device in release mode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant