Skip to content

Commit 4b96f20

Browse files
zichanggcommit-bot@chromium.org
authored andcommitted
[dart:io] Add Abort() on HttpClientRequest
The breaking change request for this cl: #41904 Bug: #22265 Change-Id: I36db64b4db307b78cd188a2f1701ec733f2e73db Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/147339 Commit-Queue: Zichang Guo <[email protected]> Reviewed-by: Lasse R.H. Nielsen <[email protected]>
1 parent 1b1a397 commit 4b96f20

File tree

5 files changed

+329
-6
lines changed

5 files changed

+329
-6
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 2.10.0
2+
3+
### Core libraries
4+
5+
#### `dart:io`
6+
7+
* Adds `Abort` method to class `HttpClientRequest`, which allows users
8+
to cancel outgoing HTTP requests and stop following IO operations.
9+
110
## 2.9.0
211

312
### Language

sdk/lib/_http/http.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2015,6 +2015,34 @@ abstract class HttpClientRequest implements IOSink {
20152015
///
20162016
/// Returns `null` if the socket is not available.
20172017
HttpConnectionInfo? get connectionInfo;
2018+
2019+
/// Aborts the client connection.
2020+
///
2021+
/// If the connection has not yet completed, the request is aborted and the
2022+
/// [done] future (also returned by [close]) is completed with the provided
2023+
/// [exception] and [stackTrace].
2024+
/// If [exception] is omitted, it defaults to an [HttpException], and if
2025+
/// [stackTrace] is omitted, it defaults to [StackTrace.empty].
2026+
///
2027+
/// If the [done] future has already completed, aborting has no effect.
2028+
///
2029+
/// Using the [IOSink] methods (e.g., [write] and [add]) has no effect after
2030+
/// the request has been aborted
2031+
///
2032+
/// ```dart
2033+
/// HttpClientRequst request = ...
2034+
/// request.write();
2035+
/// Timer(Duration(seconds: 1), () {
2036+
/// request.abort();
2037+
/// });
2038+
/// request.close().then((response) {
2039+
/// // If response comes back before abort, this callback will be called.
2040+
/// }, onError: (e) {
2041+
/// // If abort() called before response is available, onError will fire.
2042+
/// });
2043+
/// ```
2044+
@Since("2.9")
2045+
void abort([Object? exception, StackTrace? stackTrace]);
20182046
}
20192047

20202048
/**

sdk/lib/_http/http_impl.dart

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,8 @@ class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
10781078

10791079
List<RedirectInfo> _responseRedirects = [];
10801080

1081+
bool _aborted = false;
1082+
10811083
_HttpClientRequest(_HttpOutgoing outgoing, Uri uri, this.method, this._proxy,
10821084
this._httpClient, this._httpClientConnection, this._timeline)
10831085
: uri = uri,
@@ -1141,7 +1143,10 @@ class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
11411143
.then((list) => list[0]);
11421144

11431145
Future<HttpClientResponse> close() {
1144-
super.close();
1146+
if (!_aborted) {
1147+
// It will send out the request.
1148+
super.close();
1149+
}
11451150
return done;
11461151
}
11471152

@@ -1161,6 +1166,9 @@ class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
11611166
_httpClientConnection.connectionInfo;
11621167

11631168
void _onIncoming(_HttpIncoming incoming) {
1169+
if (_aborted) {
1170+
return;
1171+
}
11641172
var response = new _HttpClientResponse(incoming, this, _httpClient);
11651173
Future<HttpClientResponse> future;
11661174
if (followRedirects && response.isRedirect) {
@@ -1183,12 +1191,21 @@ class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
11831191
} else {
11841192
future = new Future<HttpClientResponse>.value(response);
11851193
}
1186-
future.then((v) => _responseCompleter.complete(v),
1187-
onError: _responseCompleter.completeError);
1194+
future.then((v) {
1195+
if (!_responseCompleter.isCompleted) {
1196+
_responseCompleter.complete(v);
1197+
}
1198+
}, onError: (e, s) {
1199+
if (!_responseCompleter.isCompleted) {
1200+
_responseCompleter.completeError(e, s);
1201+
}
1202+
});
11881203
}
11891204

11901205
void _onError(error, StackTrace stackTrace) {
1191-
_responseCompleter.completeError(error, stackTrace);
1206+
if (!_responseCompleter.isCompleted) {
1207+
_responseCompleter.completeError(error, stackTrace);
1208+
}
11921209
}
11931210

11941211
// Generate the request URI based on the method and proxy.
@@ -1221,7 +1238,21 @@ class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
12211238
}
12221239
}
12231240

1241+
void add(List<int> data) {
1242+
if (data.length == 0 || _aborted) return;
1243+
super.add(data);
1244+
}
1245+
1246+
void write(Object? obj) {
1247+
if (_aborted) return;
1248+
super.write(obj);
1249+
}
1250+
12241251
void _writeHeader() {
1252+
if (_aborted) {
1253+
_outgoing.setHeader(Uint8List(0), 0);
1254+
return;
1255+
}
12251256
BytesBuilder buffer = new _CopyingBytesBuilder(_OUTGOING_BUFFER_SIZE);
12261257

12271258
// Write the request method.
@@ -1254,6 +1285,15 @@ class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
12541285
Uint8List headerBytes = buffer.takeBytes();
12551286
_outgoing.setHeader(headerBytes, headerBytes.length);
12561287
}
1288+
1289+
void abort([Object? exception, StackTrace? stackTrace]) {
1290+
_aborted = true;
1291+
if (!_responseCompleter.isCompleted) {
1292+
exception ??= HttpException("Request has been aborted");
1293+
_responseCompleter.completeError(exception, stackTrace);
1294+
_httpClientConnection.destroy();
1295+
}
1296+
}
12571297
}
12581298

12591299
// Used by _HttpOutgoing as a target of a chunked converter for gzip

tests/standalone/io/http_client_connect_test.dart

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,126 @@ Future<void> testMaxConnectionsWithFailure() async {
308308
}
309309
}
310310

311-
void main() {
311+
Future<void> testHttpAbort() async {
312+
// Test that abort() is called after request is sent.
313+
asyncStart();
314+
final completer = Completer<void>();
315+
final server = await HttpServer.bind("127.0.0.1", 0);
316+
server.listen((request) {
317+
completer.complete();
318+
request.response.close();
319+
});
320+
321+
final request = await HttpClient().get("127.0.0.1", server.port, "/");
322+
request.headers.add(HttpHeaders.contentLengthHeader, "8");
323+
request.write('somedata');
324+
completer.future.then((_) {
325+
request.abort();
326+
asyncStart();
327+
Future.delayed(Duration(milliseconds: 500), () {
328+
server.close();
329+
asyncEnd();
330+
});
331+
});
332+
request.close().then((response) {
333+
Expect.fail('abort() prevents a response being returned');
334+
}, onError: (e) {
335+
Expect.type<HttpException>(e);
336+
Expect.isTrue(e.toString().contains('abort'));
337+
asyncEnd();
338+
});
339+
}
340+
341+
Future<void> testHttpAbortBeforeWrite() async {
342+
// Test that abort() is called before write(). No message should be sent from
343+
// HttpClientRequest.
344+
asyncStart();
345+
final completer = Completer<Socket>();
346+
final server = await ServerSocket.bind("127.0.0.1", 0);
347+
server.listen((s) async {
348+
s.listen((data) {
349+
Expect.fail('No message should be received');
350+
});
351+
await Future.delayed(Duration(milliseconds: 500));
352+
completer.complete(s);
353+
});
354+
355+
final request = await HttpClient().get("127.0.0.1", server.port, "/");
356+
request.headers.add(HttpHeaders.contentLengthHeader, "8");
357+
// This HttpException will go to onError callback.
358+
request.abort(HttpException('Error'));
359+
asyncStart();
360+
request.write('somedata');
361+
completer.future.then((socket) {
362+
socket.destroy();
363+
server.close();
364+
asyncEnd();
365+
});
366+
request.close().then((response) {
367+
Expect.fail('abort() prevents a response being returned');
368+
}, onError: (e) {
369+
Expect.type<HttpException>(e);
370+
asyncEnd();
371+
});
372+
}
373+
374+
Future<void> testHttpAbortBeforeClose() async {
375+
// Test that abort() is called after write(). Some messages added prior to
376+
// abort() are sent.
377+
final completer = new Completer<void>();
378+
asyncStart();
379+
final server = await ServerSocket.bind("127.0.0.1", 0);
380+
server.listen((s) {
381+
s.listen((data) {
382+
Expect.isTrue(utf8.decode(data).contains("content-length: 8"));
383+
completer.complete();
384+
s.destroy();
385+
server.close();
386+
asyncEnd();
387+
});
388+
});
389+
390+
final request = await HttpClient().get("127.0.0.1", server.port, "/");
391+
// Add an additional header field for server to verify.
392+
request.headers.add(HttpHeaders.contentLengthHeader, "8");
393+
request.write('somedata');
394+
await completer.future;
395+
final string = 'abort message';
396+
asyncStart();
397+
request.abort(string);
398+
request.close().then((response) {
399+
Expect.fail('abort() prevents a response being returned');
400+
}, onError: (e) {
401+
Expect.type<String>(e);
402+
Expect.equals(string, e);
403+
asyncEnd();
404+
});
405+
}
406+
407+
Future<void> testHttpAbortAfterClose() async {
408+
// Test that abort() is called after response is received. It should not
409+
// affect HttpClientResponse.
410+
asyncStart();
411+
final value = 'someRandomData';
412+
final server = await HttpServer.bind("127.0.0.1", 0);
413+
server.listen((request) {
414+
request.response.write(value);
415+
request.response.close();
416+
});
417+
418+
final request = await HttpClient().get("127.0.0.1", server.port, "/");
419+
request.close().then((response) {
420+
request.abort();
421+
response.listen((data) {
422+
Expect.equals(utf8.decode(data), value);
423+
}, onDone: () {
424+
asyncEnd();
425+
server.close();
426+
});
427+
});
428+
}
429+
430+
void main() async {
312431
testGetEmptyRequest();
313432
testGetDataRequest();
314433
testGetInvalidHost();
@@ -324,4 +443,8 @@ void main() {
324443
testMaxConnectionsPerHost(5, 10);
325444
testMaxConnectionsPerHost(10, 50);
326445
testMaxConnectionsWithFailure();
446+
await testHttpAbort();
447+
await testHttpAbortBeforeWrite();
448+
await testHttpAbortBeforeClose();
449+
await testHttpAbortAfterClose();
327450
}

0 commit comments

Comments
 (0)