Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abort HttpRequests when CancelToken is cancelled #1818

Merged
merged 3 commits into from May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions dio/CHANGELOG.md
Expand Up @@ -7,6 +7,7 @@
- Fix `IOHttpClientAdapter.onHttpClientCreate` Repeated calls
- `IOHttpClientAdapter.onHttpClientCreate` has been deprecated and is scheduled for removal in
Dio 6.0.0 - Please use the replacement `IOHttpClientAdapter.createHttpClient` instead.
- Using `CancelToken` no longer closes and re-creates `HttpClient` for each request when `IOHttpClientAdapter` is used.

## 5.1.2

Expand Down
36 changes: 24 additions & 12 deletions dio/lib/src/adapters/io_adapter.dart
Expand Up @@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:io';
import 'dart:typed_data';

import 'package:async/async.dart';

import '../adapter.dart';
import '../dio_exception.dart';
import '../options.dart';
Expand Down Expand Up @@ -65,7 +67,23 @@ class IOHttpClientAdapter implements HttpClientAdapter {
"Can't establish connection after the adapter was closed!",
);
}
final httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
final operation = CancelableOperation.fromFuture(_fetch(
options,
requestStream,
cancelFuture,
));
if (cancelFuture != null) {
cancelFuture.whenComplete(() => operation.cancel());
}
return operation.value;
}

Future<ResponseBody> _fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
final httpClient = _configHttpClient(options.connectTimeout);
final reqFuture = httpClient.openUrl(options.method, options.uri);

late HttpClientRequest request;
Expand All @@ -85,6 +103,10 @@ class IOHttpClientAdapter implements HttpClientAdapter {
request = await reqFuture;
}

if (cancelFuture != null) {
cancelFuture.whenComplete(() => request.abort());
}

// Set Headers
options.headers.forEach((k, v) {
if (v != null) request.headers.set(k, v);
Expand Down Expand Up @@ -198,17 +220,7 @@ class IOHttpClientAdapter implements HttpClientAdapter {
);
}

HttpClient _configHttpClient(
Future<void>? cancelFuture,
Duration? connectionTimeout,
) {
if (cancelFuture != null) {
final client = _createHttpClient();
client.userAgent = null;
client.idleTimeout = Duration(seconds: 0);
cancelFuture.whenComplete(() => client.close(force: true));
return client..connectionTimeout = connectionTimeout;
}
HttpClient _configHttpClient(Duration? connectionTimeout) {
return (_cachedHttpClient ??= _createHttpClient())
..connectionTimeout = connectionTimeout;
}
Expand Down
3 changes: 2 additions & 1 deletion dio/pubspec.yaml
Expand Up @@ -15,6 +15,7 @@ environment:
sdk: '>=2.15.0 <3.0.0'

dependencies:
async: ^2.8.2
http_parser: ^4.0.0
meta: ^1.5.0
path: ^1.8.0
Expand All @@ -24,5 +25,5 @@ dev_dependencies:
test: ^1.5.1
coverage: ^1.0.3
crypto: ^3.0.2
mockito: ^5.2.0
mockito: ^5.3.0
build_runner: any
58 changes: 58 additions & 0 deletions dio/test/cancel_token_test.dart
@@ -1,6 +1,10 @@
import 'package:dio/dio.dart';
import 'package:dio/src/adapters/io_adapter.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

import 'mock/http_mock.mocks.dart';

void main() {
group(CancelToken, () {
test('cancel returns the correct DioException', () async {
Expand All @@ -19,5 +23,59 @@ void main() {
test('cancel without use does not throw (#1765)', () async {
CancelToken().cancel();
});

test('cancels multiple requests', () async {
kuhnroyal marked this conversation as resolved.
Show resolved Hide resolved
final client = MockHttpClient();
final token = CancelToken();
const reason = 'cancel';
final dio = Dio()
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () => client,
);

final requests = <MockHttpClientRequest>[];
when(client.openUrl(any, any)).thenAnswer((_) async {
final request = MockHttpClientRequest();
requests.add(request);
when(request.close()).thenAnswer((_) async {
final response = MockHttpClientResponse();
when(response.headers).thenReturn(MockHttpHeaders());
when(response.statusCode).thenReturn(200);
await Future.delayed(const Duration(milliseconds: 200));
return response;
});
return request;
});

final futures = [
dio.get('https://pub.dev', cancelToken: token),
dio.get('https://pub.dev', cancelToken: token),
];

for (final future in futures) {
expectLater(
future,
throwsA((error) =>
error is DioError &&
error.type == DioErrorType.cancel &&
error.error == reason),
);
}

await Future.delayed(const Duration(milliseconds: 5));
token.cancel(reason);

expect(requests, hasLength(2));

try {
await Future.wait(futures);
} catch (_) {
// ignore, just waiting here till all futures are completed
}

for (final request in requests) {
verify(request.abort()).called(1);
}
});
});
}
10 changes: 5 additions & 5 deletions dio/test/mock/http_mock.dart
Expand Up @@ -6,11 +6,11 @@ import 'http_mock.mocks.dart';

final httpClientMock = MockHttpClient();

@GenerateMocks([
HttpClient,
HttpClientRequest,
HttpClientResponse,
HttpHeaders,
@GenerateNiceMocks([
MockSpec<HttpClient>(),
MockSpec<HttpClientRequest>(),
MockSpec<HttpClientResponse>(),
MockSpec<HttpHeaders>(),
])
class MockHttpOverrides extends HttpOverrides {
@override
Expand Down