Skip to content

Commit

Permalink
feat: add IoCompatibleClient
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto committed Sep 27, 2024
1 parent 897c13e commit fecc5b0
Show file tree
Hide file tree
Showing 15 changed files with 608 additions and 44 deletions.
4 changes: 3 additions & 1 deletion rhttp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## 0.7.3
## 0.8.0

- feat: add `IoCompatibleClient`, a compatibility layer for dart:io's `HttpClient`
- feat: add `dnsSettings` to `ClientSettings` to provide custom DNS servers
- **BREAKING**: `timeout` and `connectTimeout` moved to `TimeoutSettings` (deprecated in 0.7.2)

## 0.7.2

Expand Down
20 changes: 19 additions & 1 deletion rhttp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The APK size will increase by 2 MB on arm64 and 6 MB if compiled for all archite
- ✅ Proxy support
- ✅ Custom DNS resolution
- ✅ Strong type safety
-Optional compatibility layer for the [http](https://pub.dev/packages/http) package
-Compatible with [dart:io](https://api.dart.dev/stable/dart-io/HttpClient-class.html), [http](https://pub.dev/packages/http), and [dio](https://pub.dev/packages/dio)

## Benchmark

Expand Down Expand Up @@ -674,6 +674,24 @@ Future<Dio> createDioClient() async {
}
```

If you are looking for a replacement for `HttpClient` of `dart:io`, you can use the `IoCompatibleClient`:

```dart
import 'dart:io';
import 'package:rhttp/rhttp.dart';
void main() async {
await Rhttp.init();
final client = await IoCompatibleClient.create();
final request = await client.getUrl(Uri.parse('https://example.com'));
final response = await request.close();
print(response.statusCode);
print(await response.transform(utf8.decoder).join());
}
```

## License

MIT License
Expand Down
73 changes: 73 additions & 0 deletions rhttp/example/lib/io_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// ignore_for_file: avoid_print

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:rhttp/rhttp.dart';

Future<void> main() async {
await Rhttp.init();
runApp(const MyApp());
}

class MyApp extends StatefulWidget {
const MyApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
HttpClient? client;
String? response;

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Test Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () async {
print('Sending request...');

final payload = {
'name': 'rhttp!',
'job': 'leader',
};

client ??= await IoCompatibleClient.create();

final req = await client!.postUrl(
Uri.parse('https://reqres.in/api/users'),
);
req.headers.add('content-type', 'application/json');

final bytes = utf8.encode(jsonEncode(payload));
req.add(bytes);

final res = await req.close();
final resText = await res.transform(utf8.decoder).toList();

print('Response: ${resText.join()}');

setState(() {
response = resText.join();
});
},
child: const Text('Test'),
),
if (response != null) Text(response!),
],
),
),
),
);
}
}
3 changes: 1 addition & 2 deletions rhttp/example/lib/stream_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,13 @@ class _MyAppState extends State<MyApp> {
};

final bytes = utf8.encode(jsonEncode(payload));
final stream = Stream.fromIterable(bytes);

final res = await Rhttp.post(
'https://reqres.in/api/users',
headers: const HttpHeaders.map({
HttpHeaderName.contentType: 'application/json',
}),
body: HttpBody.stream(stream, length: bytes.length),
body: HttpBody.stream(Stream.fromIterable([bytes]), length: bytes.length),
);

print('Got response: $res');
Expand Down
2 changes: 1 addition & 1 deletion rhttp/example/macos/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Cocoa
import FlutterMacOS

@NSApplicationMain
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
Expand Down
1 change: 1 addition & 0 deletions rhttp/lib/rhttp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ library rhttp;

export 'src/client/compatible_client.dart'
show RhttpCompatibleClient, RhttpWrappedClientException;
export 'src/client/io/io_client.dart' show IoCompatibleClient;
export 'src/client/rhttp_client.dart' show RhttpClient;
export 'src/interceptor/interceptor.dart'
show
Expand Down
3 changes: 3 additions & 0 deletions rhttp/lib/src/client/compatible_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class RhttpCompatibleClient with BaseClient {

RhttpCompatibleClient._(this.client);

/// Creates a new HTTP client asynchronously.
/// Use this method if your app is already running to avoid blocking the UI.
static Future<RhttpCompatibleClient> create({
ClientSettings? settings,
List<Interceptor>? interceptors,
Expand All @@ -33,6 +35,7 @@ class RhttpCompatibleClient with BaseClient {
return RhttpCompatibleClient._(client);
}

/// Creates a new HTTP client synchronously.
/// Use this method if your app is starting up to simplify the code
/// that might arise by using async/await.
///
Expand Down
177 changes: 177 additions & 0 deletions rhttp/lib/src/client/io/io_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import 'dart:async';
import 'dart:io';

import 'package:rhttp/src/client/io/io_request.dart';
import 'package:rhttp/src/client/rhttp_client.dart';
import 'package:rhttp/src/interceptor/interceptor.dart';
import 'package:rhttp/src/model/settings.dart';

/// An HTTP client that is compatible with dart:io package.
/// This minimizes the changes needed to switch from dart:io to `rhttp`
/// and also avoids vendor lock-in.
class IoCompatibleClient implements HttpClient {
/// The actual client that is used to make requests.
final RhttpClient client;

IoCompatibleClient._(this.client);

/// Creates a new HTTP client asynchronously.
/// Use this method if your app is already running to avoid blocking the UI.
static Future<IoCompatibleClient> create({
ClientSettings? settings,
List<Interceptor>? interceptors,
}) async {
final rhttpClient = await RhttpClient.create(
settings: (settings ?? const ClientSettings()),
interceptors: interceptors,
);
return IoCompatibleClient._(rhttpClient);
}

/// Creates a new HTTP client synchronously.
/// Use this method if your app is starting up to simplify the code
/// that might arise by using async/await.
///
/// Note:
/// This method crashes when configured to use HTTP/3.
/// See: https://github.com/Tienisto/rhttp/issues/10
factory IoCompatibleClient.createSync({
ClientSettings? settings,
List<Interceptor>? interceptors,
}) {
final rhttpClient = RhttpClient.createSync(
settings: (settings ?? const ClientSettings()),
interceptors: interceptors,
);
return IoCompatibleClient._(rhttpClient);
}

/// Creates a new request.
///
/// Note:
/// The request is not sent
/// until you add a body or call [HttpClientRequest.close].
Future<RhttpIoRequest> _createRequest(String method, Uri url) async {
return RhttpIoRequest(client, method, url);
}

@override
Future<HttpClientRequest> get(String host, int port, String path) =>
open('GET', host, port, path);

@override
Future<HttpClientRequest> getUrl(Uri url) => openUrl('GET', url);

@override
Future<HttpClientRequest> post(String host, int port, String path) =>
open('POST', host, port, path);

@override
Future<HttpClientRequest> postUrl(Uri url) => openUrl('POST', url);

@override
Future<HttpClientRequest> put(String host, int port, String path) =>
open('PUT', host, port, path);

@override
Future<HttpClientRequest> putUrl(Uri url) => openUrl('PUT', url);

@override
Future<HttpClientRequest> delete(String host, int port, String path) =>
open('DELETE', host, port, path);

@override
Future<HttpClientRequest> deleteUrl(Uri url) => openUrl('DELETE', url);

@override
Future<HttpClientRequest> head(String host, int port, String path) =>
open('HEAD', host, port, path);

@override
Future<HttpClientRequest> headUrl(Uri url) => openUrl('HEAD', url);

@override
Future<HttpClientRequest> patch(String host, int port, String path) =>
open('PATCH', host, port, path);

@override
Future<HttpClientRequest> patchUrl(Uri url) => openUrl('PATCH', url);

@override
Future<HttpClientRequest> open(
String method, String host, int port, String path) =>
openUrl(method, Uri.parse('http://$host:$port$path'));

@override
Future<HttpClientRequest> openUrl(String method, Uri url) =>
_createRequest(method, url);

@override
bool autoUncompress = true;

@override
Duration? connectionTimeout;

@override
Duration get idleTimeout =>
client.settings.timeoutSettings?.keepAliveTimeout ?? Duration.zero;

@override
set idleTimeout(Duration _) =>
throw UnsupportedError('Setting idleTimeout is not supported');

@override
int? maxConnectionsPerHost;

@override
String? userAgent;

@override
void addCredentials(
Uri url, String realm, HttpClientCredentials credentials) =>
throw UnimplementedError();

@override
void addProxyCredentials(String host, int port, String realm,
HttpClientCredentials credentials) =>
throw UnimplementedError();

@override
set authenticate(
Future<bool> Function(Uri url, String scheme, String? realm)? f) =>
throw UnimplementedError();

@override
set authenticateProxy(
Future<bool> Function(
String host, int port, String scheme, String? realm)?
f) =>
throw UnimplementedError();

@override
set badCertificateCallback(
bool Function(X509Certificate cert, String host, int port)?
callback) =>
UnimplementedError();

@override
void close({bool force = false}) {
client.dispose(cancelRunningRequests: force);
}

@override
set connectionFactory(
Future<ConnectionTask<Socket>> Function(
Uri url,
String? proxyHost,
int? proxyPort,
)? f) {
UnimplementedError();
}

@override
set findProxy(String Function(Uri url)? f) => UnimplementedError();

@override
set keyLog(Function(String line)? callback) => throw UnimplementedError();
}
Loading

0 comments on commit fecc5b0

Please sign in to comment.