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, ''))));
+    });
+  });
+}