import 'package:http/http.dart'; import 'package:mangayomi/services/http/rhttp/src/client/rhttp_client.dart'; import 'package:mangayomi/services/http/rhttp/src/model/exception.dart'; import 'package:mangayomi/services/http/rhttp/src/model/settings.dart'; import 'package:mangayomi/src/rust/api/rhttp/http.dart' as rust; /// An HTTP client that is compatible with the `http` package. /// This minimizes the changes needed to switch from `http` to `rhttp` /// and also avoids vendor lock-in. /// /// This comes with some downsides, such as: /// - inferior type safety due to the flaw that `body` is of type `Object?` /// instead of a sane supertype. /// - body of type [Map] is implicitly interpreted as `x-www-form-urlencoded` /// that is only documented in StackOverflow (as of writing this). /// - no support for cancellation /// - no out-of-the-box support for multipart requests class RhttpCompatibleClient with BaseClient { /// The actual client that is used to make requests. final RhttpClient client; RhttpCompatibleClient._(this.client); /// 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 RhttpCompatibleClient.createSync({ ClientSettings? settings, }) { final client = RhttpClient.createSync( settings: (settings ?? const ClientSettings()).digest(), ); return RhttpCompatibleClient._(client); } @override Future send(BaseRequest request) async { try { final response = await client.requestStream( method: switch (request.method) { 'GET' => rust.HttpMethod.get_, 'POST' => rust.HttpMethod.post, 'PUT' => rust.HttpMethod.put, 'PATCH' => rust.HttpMethod.patch, 'DELETE' => rust.HttpMethod.delete, 'HEAD' => rust.HttpMethod.head, 'OPTIONS' => rust.HttpMethod.options, 'TRACE' => rust.HttpMethod.trace, 'CONNECT' => rust.HttpMethod.connect, _ => throw ArgumentError('Unsupported method: ${request.method}'), }, url: request.url.toString(), headers: rust.HttpHeaders.map(request.headers), body: await request.finalize().toBytes(), ); final responseHeaderMap = response.headerMap; return StreamedResponse( response.body, response.statusCode, contentLength: switch (responseHeaderMap['content-length']) { String s => int.parse(s), null => null, }, request: request, headers: responseHeaderMap, isRedirect: false, persistentConnection: true, reasonPhrase: null, ); } on RhttpException catch (e, st) { Error.throwWithStackTrace( RhttpWrappedClientException(e.toString(), request.url, e), st, ); } catch (e, st) { Error.throwWithStackTrace( ClientException(e.toString(), request.url), st, ); } } @override void close() { client.dispose(cancelRunningRequests: true); } } /// Every exception must be a subclass of [ClientException] /// as per contract of [BaseClient]. class RhttpWrappedClientException extends ClientException { /// The original exception that was thrown by rhttp. final RhttpException rhttpException; RhttpWrappedClientException(super.message, super.uri, this.rhttpException); @override String toString() => rhttpException.toString(); } extension on ClientSettings { /// Makes sure that the settings conform to the requirements of [BaseClient]. ClientSettings digest() { ClientSettings settings = this; if (throwOnStatusCode) { settings = settings.copyWith( throwOnStatusCode: false, ); } return settings; } }