Skip to content

Commit 28e05aa

Browse files
brianquinlanJaffaKetchup
authored andcommitted
Conformance tests for aborting requests
1 parent 88b9692 commit 28e05aa

File tree

5 files changed

+192
-1
lines changed

5 files changed

+192
-1
lines changed

pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:http/http.dart';
66

7+
import 'src/abort_tests.dart';
78
import 'src/close_tests.dart';
89
import 'src/compressed_response_body_tests.dart';
910
import 'src/isolate_test.dart';
@@ -22,6 +23,7 @@ import 'src/response_headers_tests.dart';
2223
import 'src/response_status_line_tests.dart';
2324
import 'src/server_errors_test.dart';
2425

26+
export 'src/abort_tests.dart' show testAbort;
2527
export 'src/close_tests.dart' show testClose;
2628
export 'src/compressed_response_body_tests.dart'
2729
show testCompressedResponseBody;
@@ -49,7 +51,7 @@ export 'src/server_errors_test.dart' show testServerErrors;
4951
//
5052
/// If [canStreamResponseBody] is `false` then tests that assume that the
5153
/// [Client] supports receiving HTTP responses with unbounded body sizes will
52-
/// be skipped
54+
/// be skipped.
5355
///
5456
/// If [redirectAlwaysAllowed] is `true` then tests that require the [Client]
5557
/// to limit redirects will be skipped.
@@ -75,6 +77,8 @@ export 'src/server_errors_test.dart' show testServerErrors;
7577
/// If [supportsMultipartRequest] is `false` then tests that assume that
7678
/// multipart requests can be sent will be skipped.
7779
///
80+
/// If [supportsAbort] is `false` then tests that assume that requests can be
81+
/// aborted will be skipped.
7882
/// The tests are run against a series of HTTP servers that are started by the
7983
/// tests. If the tests are run in the browser, then the test servers are
8084
/// started in another process. Otherwise, the test servers are run in-process.
@@ -90,6 +94,8 @@ void testAll(
9094
bool canSendCookieHeaders = false,
9195
bool canReceiveSetCookieHeaders = false,
9296
bool supportsMultipartRequest = true,
97+
// TODO: make this false, for now true to see what breaks.
98+
bool supportsAbort = true,
9399
}) {
94100
testRequestBody(clientFactory());
95101
testRequestBodyStreamed(clientFactory(),
@@ -116,4 +122,8 @@ void testAll(
116122
canSendCookieHeaders: canSendCookieHeaders);
117123
testResponseCookies(clientFactory(),
118124
canReceiveSetCookieHeaders: canReceiveSetCookieHeaders);
125+
testAbort(clientFactory(),
126+
supportsAbort: supportsAbort,
127+
canStreamRequestBody: canStreamRequestBody,
128+
canStreamResponseBody: canStreamResponseBody);
119129
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:io';
7+
8+
import 'package:async/async.dart';
9+
import 'package:stream_channel/stream_channel.dart';
10+
11+
/// Starts an HTTP server that sends a stream of integers.
12+
///
13+
/// Channel protocol:
14+
/// On Startup:
15+
/// - send port
16+
/// When Receive Anything:
17+
/// - close current request
18+
/// - exit server
19+
void hybridMain(StreamChannel<Object?> channel) async {
20+
final channelQueue = StreamQueue(channel.stream);
21+
22+
late HttpServer server;
23+
server = (await HttpServer.bind('localhost', 0))
24+
..listen((request) async {
25+
// TODO: might have to ignore exceptions in the server because it will
26+
// probably be disconnected
27+
28+
await request.drain<void>();
29+
request.response.headers.set('Access-Control-Allow-Origin', '*');
30+
request.response.headers.set('Content-Type', 'text/plain');
31+
32+
for (var i = 0; i < 10000; ++i) {
33+
request.response.write('$i\n');
34+
await request.response.flush();
35+
// Let the event loop run.
36+
await Future<void>.delayed(const Duration());
37+
}
38+
await request.response.close();
39+
});
40+
41+
channel.sink.add(server.port);
42+
unawaited(channelQueue.next.then((value) => unawaited(server.close())));
43+
}

pkgs/http_client_conformance_tests/lib/src/abort_server_vm.dart

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkgs/http_client_conformance_tests/lib/src/abort_server_web.dart

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:async/async.dart';
6+
import 'package:http/http.dart';
7+
import 'package:stream_channel/stream_channel.dart';
8+
import 'package:test/test.dart';
9+
10+
import 'abort_server_vm.dart'
11+
if (dart.library.js_interop) 'abort_server_web.dart';
12+
13+
/// Tests that the client supports aborting requests.
14+
///
15+
/// If [supportsAbort] is `false` then tests that assume that requests can be
16+
/// aborted will be skipped.
17+
///
18+
/// If [canStreamResponseBody] is `false` then tests that assume that the
19+
/// [Client] supports receiving HTTP responses with unbounded body sizes will
20+
/// be skipped.
21+
///
22+
/// If [canStreamRequestBody] is `false` then tests that assume that the
23+
/// [Client] supports sending HTTP requests with unbounded body sizes will be
24+
/// skipped.
25+
void testAbort(
26+
Client client, {
27+
bool supportsAbort = true,
28+
bool canStreamRequestBody = true,
29+
bool canStreamResponseBody = true,
30+
}) {
31+
group('abort', () {
32+
late String host;
33+
late StreamChannel<Object?> httpServerChannel;
34+
late StreamQueue<Object?> httpServerQueue;
35+
late Uri serverUrl;
36+
37+
setUp(() async {
38+
httpServerChannel = await startServer();
39+
httpServerQueue = StreamQueue(httpServerChannel.stream);
40+
host = 'localhost:${await httpServerQueue.nextAsInt}';
41+
serverUrl = Uri.http(host, '');
42+
});
43+
tearDownAll(() => httpServerChannel.sink.add(null));
44+
45+
test('before request', () async {
46+
final request = Request('GET', serverUrl);
47+
48+
// TODO: Trigger abort
49+
50+
expect(
51+
client.send(request),
52+
throwsA(
53+
isA<ClientException>().having((e) => e.uri, 'uri', serverUrl)));
54+
});
55+
56+
test('during request stream', () async {
57+
final request = StreamedRequest('POST', serverUrl);
58+
59+
final response = client.send(request);
60+
request.sink.add('Hello World'.codeUnits);
61+
// TODO: Trigger abort
62+
63+
expect(
64+
response,
65+
throwsA(
66+
isA<ClientException>().having((e) => e.uri, 'uri', serverUrl)));
67+
await request
68+
.sink.done; // Verify that the stream subscription was cancelled.
69+
}, skip: canStreamRequestBody ? false : 'does not stream request bodies');
70+
71+
test('after response', () async {
72+
final request = Request('GET', serverUrl);
73+
74+
final response = await client.send(request);
75+
76+
// TODO: Trigger abort
77+
78+
expect(
79+
response.stream.single,
80+
throwsA(
81+
isA<ClientException>().having((e) => e.uri, 'uri', serverUrl)));
82+
});
83+
84+
test('while streaming response', () async {
85+
final request = Request('GET', serverUrl);
86+
87+
final response = await client.send(request);
88+
89+
var i = 0;
90+
expect(
91+
response.stream.listen((data) {
92+
++i;
93+
if (i == 1000) {
94+
// TODO: Trigger abort
95+
}
96+
}).asFuture<void>(),
97+
throwsA(
98+
isA<ClientException>().having((e) => e.uri, 'uri', serverUrl)));
99+
expect(i, 1000);
100+
});
101+
102+
test('after streaming response', () async {
103+
final request = Request('GET', serverUrl);
104+
105+
final response = await client.send(request);
106+
await response.stream.drain<void>();
107+
// Trigger abort, should have no effect.
108+
});
109+
});
110+
}

0 commit comments

Comments
 (0)