Implement the ability to run a particular Client implementation in a Zone (#697)
diff --git a/lib/http.dart b/lib/http.dart
index 1ea751e..34eebbb 100644
--- a/lib/http.dart
+++ b/lib/http.dart
@@ -16,7 +16,7 @@
export 'src/base_request.dart';
export 'src/base_response.dart';
export 'src/byte_stream.dart';
-export 'src/client.dart';
+export 'src/client.dart' hide zoneClient;
export 'src/exception.dart';
export 'src/multipart_file.dart';
export 'src/multipart_request.dart';
diff --git a/lib/src/client.dart b/lib/src/client.dart
index e1ee89a..56ddcbc 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -2,11 +2,13 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
-import '../http.dart' as http;
+import 'package:meta/meta.dart';
+import '../http.dart' as http;
import 'base_client.dart';
import 'base_request.dart';
import 'client_stub.dart'
@@ -32,7 +34,7 @@
///
/// Creates an `IOClient` if `dart:io` is available and a `BrowserClient` if
/// `dart:html` is available, otherwise it will throw an unsupported error.
- factory Client() => createClient();
+ factory Client() => zoneClient ?? createClient();
/// Sends an HTTP HEAD request with the given headers to the given URL.
///
@@ -145,3 +147,45 @@
/// do so can cause the Dart process to hang.
void close();
}
+
+/// The [Client] for the current [Zone], if one has been set.
+///
+/// NOTE: This property is explicitly hidden from the public API.
+@internal
+Client? get zoneClient {
+ final client = Zone.current[#_clientToken];
+ return client == null ? null : (client as Client Function())();
+}
+
+/// Runs [body] in its own [Zone] with the [Client] returned by [clientFactory]
+/// set as the default [Client].
+///
+/// For example:
+///
+/// ```
+/// class MyAndroidHttpClient extends BaseClient {
+/// @override
+/// Future<http.StreamedResponse> send(http.BaseRequest request) {
+/// // your implementation here
+/// }
+/// }
+///
+/// void main() {
+/// Client client = Platform.isAndroid ? MyAndroidHttpClient() : Client();
+/// runWithClient(myFunction, () => client);
+/// }
+///
+/// void myFunction() {
+/// // Uses the `Client` configured in `main`.
+/// final response = await get(Uri.parse("https://www.example.com/"));
+/// final client = Client();
+/// }
+/// ```
+///
+/// The [Client] returned by [clientFactory] is used by the [Client.new] factory
+/// and the convenience HTTP functions (e.g. [http.get])
+R runWithClient<R>(R Function() body, Client Function() clientFactory,
+ {ZoneSpecification? zoneSpecification}) =>
+ runZoned(body,
+ zoneValues: {#_clientToken: Zone.current.bindCallback(clientFactory)},
+ zoneSpecification: zoneSpecification);
diff --git a/test/io/client_test.dart b/test/io/client_test.dart
index 2115e1c..d03a4bb 100644
--- a/test/io/client_test.dart
+++ b/test/io/client_test.dart
@@ -12,6 +12,20 @@
import '../utils.dart';
+class TestClient extends http.BaseClient {
+ @override
+ Future<http.StreamedResponse> send(http.BaseRequest request) {
+ throw UnimplementedError();
+ }
+}
+
+class TestClient2 extends http.BaseClient {
+ @override
+ Future<http.StreamedResponse> send(http.BaseRequest request) {
+ throw UnimplementedError();
+ }
+}
+
void main() {
late Uri serverUrl;
setUpAll(() async {
@@ -133,4 +147,30 @@
expect(socket, isNotNull);
});
+
+ test('runWithClient', () {
+ http.Client client =
+ http.runWithClient(() => http.Client(), () => TestClient());
+ expect(client, isA<TestClient>());
+ });
+
+ test('runWithClient nested', () {
+ late final http.Client client;
+ late final http.Client nestedClient;
+ http.runWithClient(() {
+ http.runWithClient(
+ () => nestedClient = http.Client(), () => TestClient2());
+ client = http.Client();
+ }, () => TestClient());
+ expect(client, isA<TestClient>());
+ expect(nestedClient, isA<TestClient2>());
+ });
+
+ test('runWithClient recursion', () {
+ // Verify that calling the http.Client() factory inside nested Zones does
+ // not provoke an infinite recursion.
+ http.runWithClient(() {
+ http.runWithClient(() => http.Client(), () => http.Client());
+ }, () => http.Client());
+ });
}
diff --git a/test/io/http_test.dart b/test/io/http_test.dart
index 7bdf7da..a551230 100644
--- a/test/io/http_test.dart
+++ b/test/io/http_test.dart
@@ -9,6 +9,13 @@
import '../utils.dart';
+class TestClient extends http.BaseClient {
+ @override
+ Future<http.StreamedResponse> send(http.BaseRequest request) {
+ throw UnimplementedError();
+ }
+}
+
void main() {
late Uri serverUrl;
setUpAll(() async {
@@ -22,6 +29,13 @@
expect(response.body, equals(''));
});
+ test('head runWithClient', () {
+ expect(
+ () => http.runWithClient(
+ () => http.head(serverUrl), () => TestClient()),
+ throwsUnimplementedError);
+ });
+
test('get', () async {
var response = await http.get(serverUrl, headers: {
'X-Random-Header': 'Value',
@@ -43,6 +57,13 @@
containsPair('x-other-header', ['Other Value']))))));
});
+ test('get runWithClient', () {
+ expect(
+ () =>
+ http.runWithClient(() => http.get(serverUrl), () => TestClient()),
+ throwsUnimplementedError);
+ });
+
test('post', () async {
var response = await http.post(serverUrl, headers: {
'X-Random-Header': 'Value',
@@ -151,6 +172,13 @@
})));
});
+ test('post runWithClient', () {
+ expect(
+ () => http.runWithClient(
+ () => http.post(serverUrl, body: 'testing'), () => TestClient()),
+ throwsUnimplementedError);
+ });
+
test('put', () async {
var response = await http.put(serverUrl, headers: {
'X-Random-Header': 'Value',
@@ -259,6 +287,13 @@
})));
});
+ test('put runWithClient', () {
+ expect(
+ () => http.runWithClient(
+ () => http.put(serverUrl, body: 'testing'), () => TestClient()),
+ throwsUnimplementedError);
+ });
+
test('patch', () async {
var response = await http.patch(serverUrl, headers: {
'X-Random-Header': 'Value',
@@ -388,6 +423,13 @@
containsPair('x-other-header', ['Other Value']))))));
});
+ test('patch runWithClient', () {
+ expect(
+ () => http.runWithClient(
+ () => http.patch(serverUrl, body: 'testing'), () => TestClient()),
+ throwsUnimplementedError);
+ });
+
test('read', () async {
var response = await http.read(serverUrl, headers: {
'X-Random-Header': 'Value',
@@ -412,6 +454,13 @@
expect(http.read(serverUrl.resolve('/error')), throwsClientException());
});
+ test('read runWithClient', () {
+ expect(
+ () => http.runWithClient(
+ () => http.read(serverUrl), () => TestClient()),
+ throwsUnimplementedError);
+ });
+
test('readBytes', () async {
var bytes = await http.readBytes(serverUrl, headers: {
'X-Random-Header': 'Value',
@@ -437,5 +486,12 @@
expect(
http.readBytes(serverUrl.resolve('/error')), throwsClientException());
});
+
+ test('readBytes runWithClient', () {
+ expect(
+ () => http.runWithClient(
+ () => http.readBytes(serverUrl), () => TestClient()),
+ throwsUnimplementedError);
+ });
});
}