Skip to content

Commit f5a38ae

Browse files
zzyzyzhenzhi.leeAlexV525
authored
feat: support request cancellation for native HTTP clients (#2434)
New versions of `http`, `cronet_http`, and `cupertino_http` support request cancellation (just like the `HttpClient` from `dart:io`). Minor change to use `AbortableRequest` and pass the `cancelFuture` to the `abortTrigger` argument. ### Additional context and info (if any) - [Add request cancellation to cupertino_http](dart-lang/http#1779) - [[cronet] Support aborting requests](dart-lang/http#1797) - [feat: support aborting HTTP requests](dart-lang/http#1773) ### Testing with ASP.NET Core API Test Flutter app (Android and iOS only): https://github.com/zzyzy/dio_lzz_fork_flutter/blob/main/lib/test_dio_request_cancellation.dart Test API endpoint: ```csharp [HttpGet("request-cancellation")] public async Task<IActionResult> TestRequestCancellation(CancellationToken cancellationToken) { try { _logger.LogInformation("Request started {RequestID} {Platform}", HttpContext.TraceIdentifier, Request.Headers["X-App-Platform"]); await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken); _logger.LogInformation("Request end {RequestID} {Platform}", HttpContext.TraceIdentifier, Request.Headers["X-App-Platform"]); } catch (TaskCanceledException) { _logger.LogInformation("Request canceled {RequestID} {Platform}", HttpContext.TraceIdentifier, Request.Headers["X-App-Platform"]); // Do nothing } return Ok(); } ``` #### Android Before: ``` info: Microsoft.Hosting.Lifetime[14] Now listening on: http://0.0.0.0:5292 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development info: Microsoft.Hosting.Lifetime[0] Content root path: D:\ws\me\repos\my-code-vault\MyToolkit\TestAspNetCoreWebApi info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB51JJSG7:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB51JJSG8:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB51JJSG9:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB51JJSGA:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB51JJSGB:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB51JJSGC:00000001 Android ``` After: ``` info: Microsoft.Hosting.Lifetime[14] Now listening on: http://0.0.0.0:5292 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development info: Microsoft.Hosting.Lifetime[0] Content root path: D:\ws\me\repos\my-code-vault\MyToolkit\TestAspNetCoreWebApi info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMAV9NHQ97:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request canceled 0HNEMAV9NHQ97:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMAV9NHQ98:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request canceled 0HNEMAV9NHQ98:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMAV9NHQ99:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request canceled 0HNEMAV9NHQ99:00000001 Android info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMAV9NHQ9A:00000001 Android ``` #### IOS Before: ``` info: Microsoft.Hosting.Lifetime[14] Now listening on: http://0.0.0.0:5292 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development info: Microsoft.Hosting.Lifetime[0] Content root path: D:\ws\me\repos\my-code-vault\MyToolkit\TestAspNetCoreWebApi info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB42PERN4:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB42PERN5:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB42PERN6:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB42PERN7:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB42PERN8:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB42PERN9:00000001 IOS ``` After: ``` info: Microsoft.Hosting.Lifetime[14] Now listening on: http://0.0.0.0:5292 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development info: Microsoft.Hosting.Lifetime[0] Content root path: D:\ws\me\repos\my-code-vault\MyToolkit\TestAspNetCoreWebApi info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB35NC5L0:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request canceled 0HNEMB35NC5L0:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB35NC5L1:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request canceled 0HNEMB35NC5L1:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB35NC5L2:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request canceled 0HNEMB35NC5L2:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB35NC5L3:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request canceled 0HNEMB35NC5L3:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB35NC5L4:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request canceled 0HNEMB35NC5L4:00000001 IOS info: TestAspNetCoreWebApi.Controllers.TestController[0] Request started 0HNEMB35NC5L5:00000001 IOS ``` --------- Signed-off-by: Zhen Zhi Lee <[email protected]> Co-authored-by: zhenzhi.lee <[email protected]> Co-authored-by: Alex Li <[email protected]>
1 parent fe1a62f commit f5a38ae

File tree

5 files changed

+97
-6
lines changed

5 files changed

+97
-6
lines changed

plugins/native_dio_adapter/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Unreleased
44

5-
*None.*
5+
- Support request cancellation for native HTTP clients via use of `AbortableRequest` (introduced in http package from version 1.5.0)
66

77
## 1.5.0
88

plugins/native_dio_adapter/lib/src/conversion_layer_adapter.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ class ConversionLayerAdapter implements HttpClientAdapter {
2323
Stream<Uint8List>? requestStream,
2424
Future<dynamic>? cancelFuture,
2525
) async {
26-
final request = await _fromOptionsAndStream(options, requestStream);
26+
final request = await _fromOptionsAndStream(
27+
options,
28+
requestStream,
29+
cancelFuture,
30+
);
2731
final response = await client.send(request);
2832
return response.toDioResponseBody(options);
2933
}
@@ -34,10 +38,12 @@ class ConversionLayerAdapter implements HttpClientAdapter {
3438
Future<BaseRequest> _fromOptionsAndStream(
3539
RequestOptions options,
3640
Stream<Uint8List>? requestStream,
41+
Future<dynamic>? cancelFuture,
3742
) async {
38-
final request = Request(
43+
final request = AbortableRequest(
3944
options.method,
4045
options.uri,
46+
abortTrigger: cancelFuture,
4147
);
4248

4349
request.headers.addAll(

plugins/native_dio_adapter/pubspec.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ dependencies:
2121
sdk: flutter
2222

2323
dio: ^5.4.0
24-
cupertino_http: '>=1.0.0 <3.0.0'
25-
cronet_http: '>=0.4.0 <2.0.0'
26-
http: ^1.0.0
24+
cupertino_http: ^2.3.0
25+
cronet_http: ^1.5.0
26+
http: ^1.5.0
2727

2828
dev_dependencies:
2929
lints: ^2.0.0
3030
flutter_test:
3131
sdk: flutter
32+
async: # transitive
3233

3334
platforms:
3435
android:

plugins/native_dio_adapter/test/client_mock.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:async/async.dart' show CancelableOperation;
12
import 'package:http/http.dart';
23

34
class CloseClientMock implements Client {
@@ -30,3 +31,38 @@ class ClientMock implements Client {
3031
throw UnimplementedError();
3132
}
3233
}
34+
35+
class AbortClientMock implements Client {
36+
bool isRequestCanceled = false;
37+
38+
@override
39+
Future<StreamedResponse> send(BaseRequest request) async {
40+
final cancellable = CancelableOperation.fromFuture(
41+
Future<void>.delayed(const Duration(seconds: 5)),
42+
);
43+
44+
if (request is Abortable) {
45+
request.abortTrigger?.whenComplete(
46+
() {
47+
cancellable.cancel();
48+
isRequestCanceled = true;
49+
},
50+
);
51+
}
52+
53+
await cancellable.valueOrCancellation();
54+
55+
if (cancellable.isCanceled) {
56+
throw AbortedError();
57+
}
58+
59+
return StreamedResponse(Stream.fromIterable([]), 200);
60+
}
61+
62+
@override
63+
void noSuchMethod(Invocation invocation) {
64+
throw UnimplementedError();
65+
}
66+
}
67+
68+
class AbortedError extends Error {}

plugins/native_dio_adapter/test/conversion_layer_adapter_test.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,52 @@ void main() {
6262

6363
expect(await resp.stream.length, 5);
6464
});
65+
66+
test('request cancellation', () {
67+
final mock = AbortClientMock();
68+
final cla = ConversionLayerAdapter(mock);
69+
final cancelToken = CancelToken();
70+
71+
Future<void>.delayed(const Duration(seconds: 1)).then(
72+
(value) {
73+
cancelToken.cancel();
74+
},
75+
);
76+
77+
expectLater(
78+
() => cla.fetch(
79+
RequestOptions(path: ''),
80+
null,
81+
cancelToken.whenCancel,
82+
),
83+
throwsA(isA<AbortedError>()),
84+
);
85+
});
86+
87+
test('request cancellation with Dio', () async {
88+
final mock = AbortClientMock();
89+
final cla = ConversionLayerAdapter(mock);
90+
final dio = Dio();
91+
dio.httpClientAdapter = cla;
92+
93+
final cancelToken = CancelToken();
94+
95+
Future<void>.delayed(const Duration(seconds: 1)).then(
96+
(value) {
97+
cancelToken.cancel();
98+
},
99+
);
100+
101+
await expectLater(
102+
() => dio.get<ResponseBody>('', cancelToken: cancelToken),
103+
throwsA(
104+
isA<DioException>().having(
105+
(e) => e.type,
106+
'type',
107+
DioExceptionType.cancel,
108+
),
109+
),
110+
);
111+
expect(mock.isRequestCanceled, true);
112+
});
65113
}

0 commit comments

Comments
 (0)