Skip to content

Commit 843c5ec

Browse files
authored
Fixes #701 IOClient should always throw ClientException (#719)
1 parent c455f8e commit 843c5ec

File tree

4 files changed

+98
-0
lines changed

4 files changed

+98
-0
lines changed

pkgs/http/lib/src/io_client.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ import 'io_streamed_response.dart';
1414
/// Used from conditional imports, matches the definition in `client_stub.dart`.
1515
BaseClient createClient() => IOClient();
1616

17+
/// Exception thrown when the underlying [HttpClient] throws a
18+
/// [SocketException].
19+
///
20+
/// Implemenents [SocketException] to avoid breaking existing users of
21+
/// [IOClient] that may catch that exception.
22+
class _ClientSocketException extends ClientException
23+
implements SocketException {
24+
final SocketException cause;
25+
_ClientSocketException(SocketException e, Uri url)
26+
: cause = e,
27+
super(e.message, url);
28+
29+
@override
30+
InternetAddress? get address => cause.address;
31+
32+
@override
33+
OSError? get osError => cause.osError;
34+
35+
@override
36+
int? get port => cause.port;
37+
}
38+
1739
/// A `dart:io`-based HTTP client.
1840
class IOClient extends BaseClient {
1941
/// The underlying `dart:io` HTTP client.
@@ -62,6 +84,8 @@ class IOClient extends BaseClient {
6284
persistentConnection: response.persistentConnection,
6385
reasonPhrase: response.reasonPhrase,
6486
inner: response);
87+
} on SocketException catch (error) {
88+
throw _ClientSocketException(error, request.url);
6589
} on HttpException catch (error) {
6690
throw ClientException(error.message, error.uri);
6791
}

pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'src/request_headers_tests.dart';
1111
import 'src/response_body_streamed_test.dart';
1212
import 'src/response_body_tests.dart';
1313
import 'src/response_headers_tests.dart';
14+
import 'src/server_errors_test.dart';
1415

1516
export 'src/redirect_tests.dart' show testRedirect;
1617
export 'src/request_body_streamed_tests.dart' show testRequestBodyStreamed;
@@ -44,4 +45,5 @@ void testAll(Client client,
4445
testRequestHeaders(client);
4546
testResponseHeaders(client);
4647
testRedirect(client, redirectAlwaysAllowed: redirectAlwaysAllowed);
48+
testServerErrors(client);
4749
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) 2022, 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:stream_channel/stream_channel.dart';
9+
10+
/// Starts an HTTP server that disconnects before sending it's headers.
11+
///
12+
/// Channel protocol:
13+
/// On Startup:
14+
/// - send port
15+
/// When Receive Anything:
16+
/// - exit
17+
void hybridMain(StreamChannel<Object?> channel) async {
18+
late HttpServer server;
19+
20+
server = (await HttpServer.bind('localhost', 0))
21+
..listen((request) async {
22+
await request.drain<void>();
23+
final socket = await request.response.detachSocket(writeHeaders: false);
24+
socket.destroy();
25+
});
26+
27+
channel.sink.add(server.port);
28+
await channel
29+
.stream.first; // Any writes indicates that the server should exit.
30+
unawaited(server.close());
31+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2022, 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 'utils.dart';
11+
12+
/// Tests that the [Client] correctly handles server errors.
13+
void testServerErrors(Client client,
14+
{bool redirectAlwaysAllowed = false}) async {
15+
group('server errors', () {
16+
late final String host;
17+
late final StreamChannel<Object?> httpServerChannel;
18+
late final StreamQueue<Object?> httpServerQueue;
19+
20+
setUpAll(() async {
21+
httpServerChannel = await startServer('server_errors_server.dart');
22+
httpServerQueue = StreamQueue(httpServerChannel.stream);
23+
host = 'localhost:${await httpServerQueue.next}';
24+
});
25+
tearDownAll(() => httpServerChannel.sink.add(null));
26+
27+
test('no such host', () async {
28+
expect(
29+
client.get(Uri.http('thisisnotahost', '')),
30+
throwsA(isA<ClientException>()
31+
.having((e) => e.uri, 'uri', Uri.http('thisisnotahost', ''))));
32+
});
33+
34+
test('disconnect', () async {
35+
expect(
36+
client.get(Uri.http(host, '')),
37+
throwsA(isA<ClientException>()
38+
.having((e) => e.uri, 'uri', Uri.http(host, ''))));
39+
});
40+
});
41+
}

0 commit comments

Comments
 (0)