118 lines
3.8 KiB
Dart
118 lines
3.8 KiB
Dart
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<StreamedResponse> 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;
|
|
}
|
|
}
|