Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion plugins/native_dio_adapter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

## Unreleased

*None.*
- Add opt-in `createFallbackAdapter` to `NativeAdapter`. On Android, when the
device has installed Cronet providers but every provider is disabled (for
example AOSP emulators or devices without Google Play services), the
supplied factory returns any `HttpClientAdapter` and requests continue via
that adapter. Detection is strictly limited to Cronet's
provider-disabled `RuntimeException`; every other Cronet error is
propagated unchanged. Adapter selection is sticky for the lifetime of the
`NativeAdapter`. Fixes
[#2444](https://github.com/cfug/dio/issues/2444).

## 1.6.0

Expand Down
34 changes: 34 additions & 0 deletions plugins/native_dio_adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,40 @@ final dioClient = Dio();
dioClient.httpClientAdapter = NativeAdapter();
```

### Opt-in Cronet provider fallback (Android)

On Android, `NativeAdapter` uses Cronet. Some devices — for example, AOSP
emulators or devices without Google Play services — install Cronet providers
but leave every provider disabled. On those devices `CronetEngine.build()`
throws and every request fails
([issue #2444](https://github.com/cfug/dio/issues/2444)).

If your application needs to support that environment, pass an opt-in
`createFallbackAdapter`. The factory is invoked **only** when the provider is
known to be disabled and lets you choose any `HttpClientAdapter`:

```dart
import 'package:dio/io.dart';
import 'package:native_dio_adapter/native_dio_adapter.dart';

dioClient.httpClientAdapter = NativeAdapter(
createFallbackAdapter: (error, stackTrace) => IOHttpClientAdapter(),
);
```

Notes:

- Detection is limited to Cronet's provider-disabled `RuntimeException`.
Connection, TLS, timeout, redirect, cancellation, and response-stream
errors remain Cronet errors and are propagated unchanged.
- The selection is sticky for the lifetime of the `NativeAdapter`. Once
Cronet is picked, later requests do not probe again; once the fallback is
picked, later requests reuse it.
- Changing adapters can change observable networking behavior: TLS
configuration, proxy handling, cookie storage, supported protocols
(HTTP/2, HTTP/3), and connection pooling. Callers opting in own that
tradeoff — pick the adapter that best matches your requirements.

### Use embedded Cronet

Starting from `cronet_http` v1.2.0,
Expand Down
1 change: 1 addition & 0 deletions plugins/native_dio_adapter/lib/native_dio_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export 'package:cupertino_http/cupertino_http.dart';

export 'src/conversion_layer_adapter.dart';
export 'src/cronet_adapter.dart';
export 'src/cronet_fallback_adapter.dart' show CreateFallbackAdapter;
export 'src/cupertino_adapter.dart';
export 'src/native_adapter.dart';
131 changes: 131 additions & 0 deletions plugins/native_dio_adapter/lib/src/cronet_fallback_adapter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import 'dart:typed_data' show Uint8List;

import 'package:cronet_http/cronet_http.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:jni/jni.dart' show JniException;

import 'cronet_adapter.dart';

/// Signature for building the fallback [HttpClientAdapter] used when the
/// default Cronet provider is unavailable on the current device.
///
/// See [NativeAdapter.new] and the package README for the intended contract.
typedef CreateFallbackAdapter = HttpClientAdapter Function(
Object error,
StackTrace stackTrace,
);

/// Exact `RuntimeException` message thrown by Chromium's Cronet API when
/// every registered `CronetProvider` on the device is disabled.
///
/// Chromium's provider-selection branch throws this exact `RuntimeException`:
/// https://chromium.googlesource.com/chromium/src/+/lkgr/components/cronet/android/api/src/org/chromium/net/CronetEngine.java
///
/// The referenced `CronetEngine.Builder.getPreferredCronetProvider` branch
/// throws this message when providers exist but all are disabled. It is
/// distinct from the separate "Unable to find any Cronet provider" error,
/// which reports that no provider was discovered at all.
const cronetProvidersDisabledMessage =
'java.lang.RuntimeException: All available Cronet providers are disabled. '
'A provider should be enabled before it can be used.';

/// Classifies the failure that indicates all installed Cronet providers on
/// the device are disabled.
///
/// `contains` is intentional: [JniException.message] also includes the Java
/// stack trace appended to the message. Do not broaden the predicate to all
/// [JniException]s or all engine-initialization failures.
bool isCronetProviderUnavailable(Object error) =>
error is JniException &&
error.message.contains(cronetProvidersDisabledMessage);

/// Builds the Cronet-backed [HttpClientAdapter] to use when Cronet is
/// available. May throw when the underlying Cronet provider is disabled or
/// otherwise unavailable.
typedef BuildCronetAdapter = HttpClientAdapter Function();

/// Android-only lazy adapter selection that either uses a [CronetAdapter] or,
/// if the default Cronet provider is known to be unavailable on the device,
/// a caller-supplied fallback [HttpClientAdapter].
///
/// The choice is sticky for the lifetime of this instance: once made, later
/// requests do not probe Cronet again. This wrapper is created only when
/// [NativeAdapter] is opted-in via `createFallbackAdapter`.
class CronetWithFallbackAdapter implements HttpClientAdapter {
/// Production constructor used by [NativeAdapter].
///
/// The "build the Cronet path" step is invoked lazily on the first
/// [fetch] call: it synchronously creates a [CronetEngine] so that the
/// provider-disabled failure surfaces here, before the request is
/// delegated to any adapter. When [createCronetEngine] or
/// [androidCronetEngine] is supplied, the caller-provided engine is used;
/// otherwise [CronetEngine.build] is invoked.
CronetWithFallbackAdapter({
required CronetEngine Function()? createCronetEngine,
required CronetEngine? androidCronetEngine,
required CreateFallbackAdapter createFallbackAdapter,
}) : _buildCronetAdapter = (() {
final engine = createCronetEngine?.call() ??
androidCronetEngine ??
CronetEngine.build();
return CronetAdapter(engine);
}),
_createFallbackAdapter = createFallbackAdapter;

/// Test-only constructor: lets a test inject a controllable "build cronet
/// adapter" seam without linking real native Cronet code. Not part of the
/// public API.
@visibleForTesting
CronetWithFallbackAdapter.forTesting({
required BuildCronetAdapter buildCronetAdapter,
required CreateFallbackAdapter createFallbackAdapter,
}) : _buildCronetAdapter = buildCronetAdapter,
_createFallbackAdapter = createFallbackAdapter;

final BuildCronetAdapter _buildCronetAdapter;
final CreateFallbackAdapter _createFallbackAdapter;

HttpClientAdapter? _selected;
bool _closed = false;

/// The adapter chosen for this instance, or `null` if selection has not
/// happened yet. Visible for tests.
@visibleForTesting
HttpClientAdapter? get selectedAdapter => _selected;

@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<dynamic>? cancelFuture,
) {
final adapter = _selectAdapter();
return adapter.fetch(options, requestStream, cancelFuture);
}

@override
void close({bool force = false}) {
if (_closed) {
return;
}
_closed = true;
// If no request was ever made, do NOT initialize Cronet just to close it.
_selected?.close(force: force);
}

HttpClientAdapter _selectAdapter() {
final existing = _selected;
if (existing != null) {
return existing;
}
try {
return _selected = _buildCronetAdapter();
} catch (error, stackTrace) {
if (isCronetProviderUnavailable(error)) {
return _selected = _createFallbackAdapter(error, stackTrace);
}
rethrow;
}
}
}
55 changes: 52 additions & 3 deletions plugins/native_dio_adapter/lib/src/native_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:cupertino_http/cupertino_http.dart';
import 'package:dio/dio.dart';

import 'cronet_adapter.dart';
import 'cronet_fallback_adapter.dart';
import 'cupertino_adapter.dart';

/// A [HttpClientAdapter] for Dio which delegates HTTP requests
Expand All @@ -17,9 +18,49 @@ import 'cupertino_adapter.dart';
/// On Android this uses [cronet_http](https://pub.dev/packages/cronet_http) to
/// make HTTP requests.
class NativeAdapter implements HttpClientAdapter {
/// Creates a [NativeAdapter].
///
/// {@template native_dio_adapter.NativeAdapter.createFallbackAdapter}
/// [createFallbackAdapter] is an **opt-in** fallback for Android devices on
/// which every installed Cronet provider is disabled (for example, AOSP
/// emulators or devices without Google Play services, see
/// [issue #2444](https://github.com/cfug/dio/issues/2444)). It is invoked
/// **only** when Cronet reports that all providers are disabled; every other
/// error — including connection, TLS, timeout, redirect, cancellation, and
/// response-stream errors — remains a Cronet error and is propagated
/// unchanged.
///
/// The factory returns any [HttpClientAdapter]. This lets callers choose an
/// adapter that matches their TLS, proxy, cookie, transport, and
/// observability requirements (for example, `IOHttpClientAdapter` from
/// `package:dio` or a custom adapter). Note: switching adapters can change
/// observable networking behavior — TLS configuration, proxy handling,
/// cookies, supported protocols, connection pooling, etc. Callers opting in
/// own that tradeoff.
///
/// Detection happens synchronously before the first request is delegated.
/// After that, the selection is sticky for the lifetime of this
/// [NativeAdapter]; later requests do not probe Cronet again. Closing this
/// [NativeAdapter] before any request is made does **not** initialize
/// Cronet or create the fallback.
///
/// Defaults to `null`. When omitted, [NativeAdapter] continues to use
/// Cronet and propagates initialization errors exactly as it did before.
/// This factory is only consulted on Android; it is ignored on other
/// platforms.
///
/// Example:
///
/// ```dart
/// NativeAdapter(
/// createFallbackAdapter: (error, stackTrace) => IOHttpClientAdapter(),
/// )
/// ```
/// {@endtemplate}
NativeAdapter({
CronetEngine Function()? createCronetEngine,
URLSessionConfiguration Function()? createCupertinoConfiguration,
CreateFallbackAdapter? createFallbackAdapter,
@Deprecated(
'Use createCronetEngine instead. '
'This will cause platform exception on iOS/macOS platforms. '
Expand All @@ -34,9 +75,17 @@ class NativeAdapter implements HttpClientAdapter {
URLSessionConfiguration? cupertinoConfiguration,
}) {
if (Platform.isAndroid) {
_adapter = CronetAdapter(
createCronetEngine?.call() ?? androidCronetEngine,
);
if (createFallbackAdapter != null) {
_adapter = CronetWithFallbackAdapter(
createCronetEngine: createCronetEngine,
androidCronetEngine: androidCronetEngine,
createFallbackAdapter: createFallbackAdapter,
);
} else {
_adapter = CronetAdapter(
createCronetEngine?.call() ?? androidCronetEngine,
);
}
} else if (Platform.isIOS || Platform.isMacOS) {
_adapter = CupertinoAdapter(
createCupertinoConfiguration?.call() ??
Expand Down
4 changes: 4 additions & 0 deletions plugins/native_dio_adapter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ dependencies:
cupertino_http: '>=2.3.0 <4.0.0'
cronet_http: ^1.5.0
http: ^1.5.0
# Direct dependency for `JniException`, used to classify the
# Cronet-provider-disabled failure surfaced by `CronetEngine.build()`.
# The constraint tracks the version resolved by `cronet_http: ^1.5.0`.
jni: ^0.14.0

dev_dependencies:
lints: ^2.0.0
Expand Down
Loading
Loading