Skip to content

Commit ca07b4c

Browse files
authored
Add request cancellation to cupertino_http (#1779)
1 parent 984cc43 commit ca07b4c

File tree

5 files changed

+54
-20
lines changed

5 files changed

+54
-20
lines changed

pkgs/cupertino_http/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## 2.2.1-wip
1+
## 2.3.0-wip
22

3+
* Add the ability to abort requests.
34
* Make `ConnectionException.toString` more helpful.
45

56
## 2.2.0

pkgs/cupertino_http/example/integration_test/client_conformance_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ void main() {
4949
canReceiveSetCookieHeaders: true,
5050
canSendCookieHeaders: true,
5151
correctlyHandlesNullHeaderValues: false,
52+
supportsAbort: true,
5253
);
5354
});
5455
}

pkgs/cupertino_http/example/pubspec.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,3 @@ dev_dependencies:
3838

3939
flutter:
4040
uses-material-design: true
41-
42-
# TODO(brianquinlan): Remove this when a release version of `package:http`
43-
# supports abortable requests.
44-
dependency_overrides:
45-
http:
46-
path: ../../http/

pkgs/cupertino_http/lib/src/cupertino_client.dart

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class _TaskTracker {
5959

6060
/// Whether the response stream subscription has been cancelled.
6161
bool responseListenerCancelled = false;
62+
bool requestAborted = false;
6263
final HttpClientRequestProfile? profile;
6364
int numRedirects = 0;
6465
Uri? lastUrl; // The last URL redirected to.
@@ -192,14 +193,23 @@ class CupertinoClient extends BaseClient {
192193
static void _onComplete(
193194
URLSession session, URLSessionTask task, NSError? error) {
194195
final taskTracker = _tracker(task);
195-
// The task will only be cancelled if the user calls
196-
// `StreamedResponse.stream.cancel()`, which can only happen if the response
197-
// has already been received. Therefore, it is safe to handle task
198-
// cancellation errors as if the response completed normally.
196+
197+
// There are two ways that the request can be cancelled:
198+
// 1. The user calls `StreamedResponse.stream.cancel()`, which can only
199+
// happen if the response has already been received.
200+
// 2. The user aborts the request, which can happen at any point in the
201+
// request lifecycle and causes `CupertinoClient.send` to throw
202+
// a `RequestAbortedException` exception.
203+
final isCancelError = error?.domain.toDartString() == 'NSURLErrorDomain' &&
204+
error?.code == _nsurlErrorCancelled;
199205
if (error != null &&
200-
!(error.domain.toDartString() == 'NSURLErrorDomain' &&
201-
error.code == _nsurlErrorCancelled)) {
202-
final exception = NSErrorClientException(error, taskTracker.request.url);
206+
!(isCancelError && taskTracker.responseListenerCancelled)) {
207+
final Exception exception;
208+
if (isCancelError) {
209+
exception = RequestAbortedException();
210+
} else {
211+
exception = NSErrorClientException(error, taskTracker.request.url);
212+
}
203213
if (taskTracker.profile != null &&
204214
taskTracker.profile!.requestData.endTime == null) {
205215
// Error occurred during the request.
@@ -230,7 +240,9 @@ class CupertinoClient extends BaseClient {
230240

231241
static void _onData(URLSession session, URLSessionTask task, NSData data) {
232242
final taskTracker = _tracker(task);
233-
if (taskTracker.responseListenerCancelled) return;
243+
if (taskTracker.responseListenerCancelled || taskTracker.requestAborted) {
244+
return;
245+
}
234246
taskTracker.responseController.add(data.toList());
235247
taskTracker.profile?.responseData.bodySink.add(data.toList());
236248
}
@@ -349,6 +361,7 @@ class CupertinoClient extends BaseClient {
349361
'Content-Length', '${request.contentLength}');
350362
}
351363

364+
NSInputStream? nsStream;
352365
if (request is Request) {
353366
// Optimize the (typical) `Request` case since assigning to
354367
// `httpBodyStream` requires a lot of expensive setup and data passing.
@@ -359,17 +372,28 @@ class CupertinoClient extends BaseClient {
359372
// then setting `httpBodyStream` will cause the request to fail -
360373
// even if the stream is empty.
361374
if (profile == null) {
362-
urlRequest.httpBodyStream = s.toNSInputStream();
375+
nsStream = s.toNSInputStream();
376+
urlRequest.httpBodyStream = nsStream;
363377
} else {
364378
final splitter = StreamSplitter(s);
365-
urlRequest.httpBodyStream = splitter.split().toNSInputStream();
379+
nsStream = splitter.split().toNSInputStream();
380+
urlRequest.httpBodyStream = nsStream;
366381
unawaited(profile.requestData.bodySink.addStream(splitter.split()));
367382
}
368383
}
369384

370385
// This will preserve Apple default headers - is that what we want?
371386
request.headers.forEach(urlRequest.setValueForHttpHeaderField);
372387
final task = urlSession.dataTaskWithRequest(urlRequest);
388+
if (request case Abortable(:final abortTrigger?)) {
389+
unawaited(abortTrigger.whenComplete(() {
390+
final taskTracker = _tasks[task];
391+
if (taskTracker == null) return;
392+
taskTracker.requestAborted = true;
393+
task.cancel();
394+
}));
395+
}
396+
373397
final subscription = StreamController<Uint8List>(onCancel: () {
374398
final taskTracker = _tasks[task];
375399
if (taskTracker == null) return;
@@ -383,7 +407,21 @@ class CupertinoClient extends BaseClient {
383407
final maxRedirects = request.followRedirects ? request.maxRedirects : 0;
384408

385409
late URLResponse result;
386-
result = await taskTracker.responseCompleter.future;
410+
try {
411+
result = await taskTracker.responseCompleter.future;
412+
} finally {
413+
// If the request is aborted before the `NSUrlSessionTask` opens the
414+
// `NSInputStream` attached to `NSMutableURLRequest.HTTPBodyStream`, then
415+
// the task will not close the `NSInputStream`.
416+
//
417+
// This will cause the Dart portion of the `NSInputStream` implementation
418+
// to hang waiting for a close message.
419+
//
420+
// See https://github.com/dart-lang/native/issues/2333
421+
if (nsStream?.streamStatus != NSStreamStatus.NSStreamStatusClosed) {
422+
nsStream?.close();
423+
}
424+
}
387425

388426
final response = result as HTTPURLResponse;
389427

pkgs/cupertino_http/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: cupertino_http
2-
version: 2.2.1-wip
2+
version: 2.3.0-wip
33
description: >-
44
A macOS/iOS Flutter plugin that provides access to the Foundation URL
55
Loading System.
@@ -14,7 +14,7 @@ dependencies:
1414
ffi: ^2.1.0
1515
flutter:
1616
sdk: flutter
17-
http: ^1.2.0
17+
http: ^1.5.0-beta
1818
http_profile: ^0.1.0
1919
objective_c: ^7.0.0
2020
web_socket: '>=0.1.5 <2.0.0'

0 commit comments

Comments
 (0)