Fixes #701 IOClient should always throw ClientException (#719)
diff --git a/pkgs/http/lib/src/io_client.dart b/pkgs/http/lib/src/io_client.dart
index b26ec04..9663187 100644
--- a/pkgs/http/lib/src/io_client.dart
+++ b/pkgs/http/lib/src/io_client.dart
@@ -14,6 +14,28 @@
/// Used from conditional imports, matches the definition in `client_stub.dart`.
BaseClient createClient() => IOClient();
+/// Exception thrown when the underlying [HttpClient] throws a
+/// [SocketException].
+///
+/// Implemenents [SocketException] to avoid breaking existing users of
+/// [IOClient] that may catch that exception.
+class _ClientSocketException extends ClientException
+ implements SocketException {
+ final SocketException cause;
+ _ClientSocketException(SocketException e, Uri url)
+ : cause = e,
+ super(e.message, url);
+
+ @override
+ InternetAddress? get address => cause.address;
+
+ @override
+ OSError? get osError => cause.osError;
+
+ @override
+ int? get port => cause.port;
+}
+
/// A `dart:io`-based HTTP client.
class IOClient extends BaseClient {
/// The underlying `dart:io` HTTP client.
@@ -62,6 +84,8 @@
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
inner: response);
+ } on SocketException catch (error) {
+ throw _ClientSocketException(error, request.url);
} on HttpException catch (error) {
throw ClientException(error.message, error.uri);
}
diff --git a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
index 58457e8..dfdc919 100644
--- a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
+++ b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
@@ -11,6 +11,7 @@
import 'src/response_body_streamed_test.dart';
import 'src/response_body_tests.dart';
import 'src/response_headers_tests.dart';
+import 'src/server_errors_test.dart';
export 'src/redirect_tests.dart' show testRedirect;
export 'src/request_body_streamed_tests.dart' show testRequestBodyStreamed;
@@ -44,4 +45,5 @@
testRequestHeaders(client);
testResponseHeaders(client);
testRedirect(client, redirectAlwaysAllowed: redirectAlwaysAllowed);
+ testServerErrors(client);
}
diff --git a/pkgs/http_client_conformance_tests/lib/src/server_errors_server.dart b/pkgs/http_client_conformance_tests/lib/src/server_errors_server.dart
new file mode 100644
index 0000000..47470a4
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/server_errors_server.dart
@@ -0,0 +1,31 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:stream_channel/stream_channel.dart';
+
+/// Starts an HTTP server that disconnects before sending it's headers.
+///
+/// Channel protocol:
+/// On Startup:
+/// - send port
+/// When Receive Anything:
+/// - exit
+void hybridMain(StreamChannel<Object?> channel) async {
+ late HttpServer server;
+
+ server = (await HttpServer.bind('localhost', 0))
+ ..listen((request) async {
+ await request.drain<void>();
+ final socket = await request.response.detachSocket(writeHeaders: false);
+ socket.destroy();
+ });
+
+ channel.sink.add(server.port);
+ await channel
+ .stream.first; // Any writes indicates that the server should exit.
+ unawaited(server.close());
+}
diff --git a/pkgs/http_client_conformance_tests/lib/src/server_errors_test.dart b/pkgs/http_client_conformance_tests/lib/src/server_errors_test.dart
new file mode 100644
index 0000000..076502f
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/server_errors_test.dart
@@ -0,0 +1,41 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:async/async.dart';
+import 'package:http/http.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+/// Tests that the [Client] correctly handles server errors.
+void testServerErrors(Client client,
+ {bool redirectAlwaysAllowed = false}) async {
+ group('server errors', () {
+ late final String host;
+ late final StreamChannel<Object?> httpServerChannel;
+ late final StreamQueue<Object?> httpServerQueue;
+
+ setUpAll(() async {
+ httpServerChannel = await startServer('server_errors_server.dart');
+ httpServerQueue = StreamQueue(httpServerChannel.stream);
+ host = 'localhost:${await httpServerQueue.next}';
+ });
+ tearDownAll(() => httpServerChannel.sink.add(null));
+
+ test('no such host', () async {
+ expect(
+ client.get(Uri.http('thisisnotahost', '')),
+ throwsA(isA<ClientException>()
+ .having((e) => e.uri, 'uri', Uri.http('thisisnotahost', ''))));
+ });
+
+ test('disconnect', () async {
+ expect(
+ client.get(Uri.http(host, '')),
+ throwsA(isA<ClientException>()
+ .having((e) => e.uri, 'uri', Uri.http(host, ''))));
+ });
+ });
+}