Skip to content

feat(native_dio_adapter): opt-in Cronet provider fallback#2573

Merged
AlexV525 merged 1 commit into
mainfrom
feat/cronet-provider-fallback
Jul 22, 2026
Merged

feat(native_dio_adapter): opt-in Cronet provider fallback#2573
AlexV525 merged 1 commit into
mainfrom
feat/cronet-provider-fallback

Conversation

@AlexV525

Copy link
Copy Markdown
Member

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().

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:

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

  • melos run format — passes.
  • melos run analyze — passes.
  • melos run test — passes (26 tests in native_dio_adapter, including 12 new).
  • 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

Add `NativeAdapter(createFallbackAdapter: ...)` for Android devices on
which every installed Cronet provider is disabled (for example AOSP
emulators or devices without Google Play services). When Cronet reports
the specific provider-disabled `RuntimeException`, the caller-supplied
factory returns any `HttpClientAdapter` and requests continue via that
adapter.

The option is opt-in and defaults to `null`: without it, `NativeAdapter`
continues to use Cronet and propagates initialization errors exactly as
before. Detection matches the exact provider-disabled message on a
`JniException`; every other Cronet error is propagated unchanged, and
adapter selection is sticky for the lifetime of the `NativeAdapter`.

Fixes #2444.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AlexV525
AlexV525 requested a review from a team as a code owner July 21, 2026 17:11
@github-actions

Copy link
Copy Markdown
Contributor

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
plugins/native_dio_adapter/lib/src/cronet_fallback_adapter.dart 🔴 0% 🟢 78.26% 🟢 78.26%
Overall Coverage 🟢 86.52% 🟢 86.09% 🔴 -0.43%

Minimum allowed coverage is 0%, this run produced 86.09%

@CaiJingLong CaiJingLong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review Conclusion: Approve

Change Summary

Adds an opt-in createFallbackAdapter factory parameter to NativeAdapter. On Android, when CronetEngine.build() throws the specific provider-disabled RuntimeException (all Cronet providers disabled, e.g. AOSP emulators / devices without Google Play services), the caller-supplied factory returns any HttpClientAdapter and requests continue via that adapter. The parameter defaults to null; without it, behavior is unchanged. Fixes #2444.

Review Details

Dimension Conclusion Notes
Correctness Pass Lazy selection in _selectAdapter() is synchronous with no await points, so no race in a single Dart isolate. The try/catch wraps only _buildCronetAdapter(), so post-init errors (connection/TLS/timeout/redirect/cancellation) from a successfully-built Cronet adapter propagate unchanged — verified by test. The classifier isCronetProviderUnavailable requires error is JniException and message.contains(cronetProvidersDisabledMessage) with the exact java.lang.RuntimeException: prefix, so a wrong exception class or a non-JniException carrying the same text is rejected. Sticky selection via _selected is correct. close() is idempotent (_closed flag) and does not build Cronet when no request was made.
Tests Pass 12 new focused tests in cronet_fallback_adapter_test.dart using a @visibleForTesting build seam and recording mock adapters. Covers: classifier positive/negative (wrong class, wrong message, non-JniException), fallback invoked exactly once with identity-preserved request/body/cancel, non-matching rethrow, ArgumentError rethrow, post-init errors do not trigger fallback, sticky reuse of both fallback and Cronet paths, close after selection (exactly once, force forwarded), close before any request (no build, no fallback). Full package suite: 26 tests pass. dart analyze clean.
Style Pass Matches existing module boundaries and naming. CreateFallbackAdapter typedef exported via show only — CronetWithFallbackAdapter, the classifier, and internals are not part of the public API. No debug remnants. dart format clean.
Risk Pass Additive, opt-in, null default — no behavior change for existing apps, no public-API break. Direct jni: ^0.14.0 dependency is justified (needed for the typed JniException match) and compatible with cronet_http: ^1.5.0's transitive jni: ^0.14.2 (resolves to 0.14.2). The eager CronetEngine.build() in the fallback path vs. the lazy build in the non-fallback path is intentional — it is the mechanism that surfaces the provider-disabled error before any request is delegated. Sensitive area (network adapter), but the change is well-isolated behind the opt-in parameter.
Docs Pass README.md adds an opt-in section with the observable-behavior caveat. CHANGELOG.md Unreleased entry is user-facing and references #2444. API doc on the new parameter is thorough (contract, stickiness, close semantics, platform scope, example).
Repo constraints Pass Branch feat/cronet-provider-fallback follows category/short-description. Commit uses Conventional Commits (feat(native_dio_adapter): ...), lowercase subject, no gitmoji+prefix mix. Co-Authored-By AI attribution present. CHANGELOG updated. No public-API break. No drive-by dependency bumps — the jni addition is the point and is documented.

Confirmed Key Points

  • Correctness: The classifier is precise (typed JniException + exact RuntimeException message via contains to tolerate the appended Java stack trace). Post-init errors are untouched because the try/catch scope is only the build seam. Selection is sticky; close() is idempotent and never initializes Cronet when no request was made.
  • Tests: 12 new tests + 14 existing = 26 pass; dart analyze and dart format clean. Coverage on the new file is 78.26%.
  • Risk: Opt-in with null default — zero behavior change for non-opting apps. The jni direct dependency is compatible with the existing transitive resolution. No public symbols removed/renamed; only an additive optional parameter and a new exported typedef.

Suggestions (non-blocking)

  • The PR body references docs/plans/cronet-provider-fallback.md, but that file is not part of the diff. Consider either including it or dropping the reference to avoid a dangling citation.
  • Android integration on an AOSP emulator without Google Play services remains unverified (acknowledged in the PR body). The unit tests use a controllable build seam and mock the JniException; real-device validation would close the loop on the exact runtime JniException.message shape.
  • Pre-existing (not introduced by this PR): the package declares sdk: '>=3.1.0' while cronet_http: ^1.5.0 transitively pulls jni: ^0.14.2 which requires sdk: '>=3.3.0'. This constraint mismatch predates this PR and is not worsened by the added direct jni: ^0.14.0 dependency.

This comment was generated by AI agent omp (model: devin/glm-5-2).

@AlexV525
AlexV525 merged commit 26ed7e5 into main Jul 22, 2026
5 checks passed
@AlexV525
AlexV525 deleted the feat/cronet-provider-fallback branch July 22, 2026 04:03
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.

[native_dio_adapter] All network requests fail when Google Play services are unavailable

2 participants