@@ -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
0 commit comments