fix(native_dio_adapter): gracefully fall back when Cronet unavailable#2479
Closed
DevMohammadSalameh wants to merge 1 commit into
Closed
fix(native_dio_adapter): gracefully fall back when Cronet unavailable#2479DevMohammadSalameh wants to merge 1 commit into
DevMohammadSalameh wants to merge 1 commit into
Conversation
Fixes cfug#2444 When Google Play Services is unavailable, CronetEngine initialization throws a JniException. This change catches the exception and falls back to the standard HttpClientAdapter using dart:io. Also adds an optional onCronetUnavailable callback for developers who want to be notified when the fallback occurs.
AlexV525
reviewed
Feb 27, 2026
| // HttpClient when unavailable (e.g., on emulators without Google APIs | ||
| // or devices without Google Play Services). | ||
| onCronetUnavailable?.call(e); | ||
| _adapter = HttpClientAdapter(); |
Member
There was a problem hiding this comment.
As #2444 (comment) said, I think this should be set as opt-in.
Comment on lines
+45
to
+49
| } catch (e) { | ||
| // Cronet requires Google Play Services. Fall back to dart:io | ||
| // HttpClient when unavailable (e.g., on emulators without Google APIs | ||
| // or devices without Google Play Services). | ||
| onCronetUnavailable?.call(e); |
Member
There was a problem hiding this comment.
Suggested change
| } catch (e) { | |
| // Cronet requires Google Play Services. Fall back to dart:io | |
| // HttpClient when unavailable (e.g., on emulators without Google APIs | |
| // or devices without Google Play Services). | |
| onCronetUnavailable?.call(e); | |
| } catch (e, s) { | |
| // Cronet requires Google Play Services. Fall back to dart:io | |
| // HttpClient when unavailable (e.g., on emulators without Google APIs | |
| // or devices without Google Play Services). | |
| onCronetUnavailable?.call(e, s); |
5 tasks
Member
|
Superseded by #2573, which takes a different approach based on the design in
Thanks for the initial PR and for surfacing the fix — it prompted the design discussion. Closing this in favor of #2573. |
AlexV525
added a commit
that referenced
this pull request
Jul 22, 2026
## Summary Fixes #2444. Adds an opt-in `createFallbackAdapter` factory to `NativeAdapter`. When Cronet reports that every installed provider on the device is disabled (for example, AOSP emulators or devices without Google Play services), the caller-supplied factory returns any `HttpClientAdapter` and requests continue via that adapter — instead of every request failing on `CronetEngine.build()`. ```dart NativeAdapter( createFallbackAdapter: (error, stackTrace) => IOHttpClientAdapter(), ) ``` ### Key properties - **Opt-in.** The parameter defaults to `null`. Without it, `NativeAdapter` continues to use Cronet and propagates initialization errors exactly as it does today. No behavior change for existing applications. - **Precise error classification.** Matches the exact Chromium provider-disabled `RuntimeException` on a typed `JniException` (adds `jni` as a direct dependency). Does **not** fall back on any other `JniException`, non-`JniException` errors carrying similar text, or engine initialization errors of other kinds. - **Post-init errors are untouched.** Connection, TLS, timeout, redirect, cancellation, and response-stream errors from a successfully-initialized Cronet path remain Cronet errors and are propagated unchanged. - **Sticky selection.** The choice is made lazily on the first `fetch` and cached; later requests do not probe Cronet again. `close()` closes the selected adapter once. Closing before the first request does not initialize Cronet or create the fallback. - **Adapter, not `Client`.** The factory returns any `HttpClientAdapter` — callers pick the one whose TLS, proxy, cookie, transport, and observability behavior matches their needs. ### Why this shape This PR replaces the two prior attempts against #2444, which were rejected on their approach: - **#2445** wrapped `CronetEngine.build()` in `CronetAdapter` and switched the internal `http.Client` mid-request. That is not safe once `ConversionLayerAdapter` has consumed the Dio request stream. - **#2479** made the fallback silent and default, fixed the fallback to `HttpClientAdapter()` (dart:io), and matched on `.toString()`. That silently changes networking behavior for existing apps and is fragile against exception-message changes. The rationale for the current design is captured in `docs/plans/cronet-provider-fallback.md`. ## Changes - `plugins/native_dio_adapter/lib/src/cronet_fallback_adapter.dart` — new Android-only lazy selection wrapper `CronetWithFallbackAdapter`, the classifier `isCronetProviderUnavailable`, and the `CreateFallbackAdapter` typedef. Includes a `@visibleForTesting` constructor that lets unit tests inject a controllable "build cronet path" seam without native code. - `plugins/native_dio_adapter/lib/src/native_adapter.dart` — new `createFallbackAdapter` parameter; wired on Android only, only when the factory is supplied. - `plugins/native_dio_adapter/lib/native_dio_adapter.dart` — export the `CreateFallbackAdapter` typedef. - `plugins/native_dio_adapter/pubspec.yaml` — add direct `jni: ^0.14.0` dependency (compatible with `cronet_http: ^1.5.0`'s transitive `^0.14.2`). - `plugins/native_dio_adapter/README.md` — opt-in example section with the observable-behavior caveat. - `plugins/native_dio_adapter/CHANGELOG.md` — Unreleased entry. - `plugins/native_dio_adapter/test/cronet_fallback_adapter_test.dart` — 12 focused tests using a controllable "build cronet adapter" seam and mock `HttpClientAdapter` implementations. ## Test plan - [x] `melos run format` — passes. - [x] `melos run analyze` — passes. - [x] `melos run test` — passes (26 tests in `native_dio_adapter`, including 12 new). - [x] Adversarial second-opinion review against the plan — no blocking issues. - [ ] Android integration on an AOSP emulator without Google Play services. This complements unit coverage; not blocking on it here. If the CI matrix cannot provide the required emulator, this should be recorded separately per the plan. ### Unit coverage (per plan) - Classified error triggers the fallback factory exactly once and forwards the unchanged request, body, and cancel future by identity. - Non-matching `JniException` (wrong exception class or wrong message) is rethrown and no fallback is created. - Non-`JniException` error carrying the same text is rejected by the classifier. - `ArgumentError` during Cronet build is rethrown, not fallen back. - Errors thrown by the successfully-built Cronet adapter (post-init connection/TLS/timeout errors) do NOT trigger a fallback. - Selected fallback and selected Cronet adapter are each reused across subsequent requests. - `close` after selection closes the selected adapter exactly once (and later `close` calls are no-ops). - `close` before any request does not invoke the build seam or create a fallback. The "no fallback factory" case is satisfied by construction — `NativeAdapter` does not instantiate the wrapper when the factory is `null`, and calling with a `null` factory is indistinguishable from prior versions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2444
When Google Play Services is unavailable (e.g., on Android emulators without Google APIs or devices without Play Services),
CronetEngineinitialization throws aJniException:This change:
CronetAdaptercreation in a try-catch blockHttpClientAdapter(dart:io) when Cronet is unavailableonCronetUnavailablecallback for developers who want to be notified when the fallback occursChanges
native_adapter.dart: Added graceful fallback with try-catchCHANGELOG.md: Documented the new behaviorUsage
Test Plan
melos run format- passesmelos run analyze- passesflutter testin native_dio_adapter - all 10 tests pass