Repository: dart-lang/http (dart)

Issue:
_ClientSocketException should have a better toString
...and possibly be public so that users who are willing to have `dart:io` specific code can handle the exception explicitly.

Pull request description:
Also explain how to catch `SocketException`.

Fixes https://github.com/dart-lang/http/issues/930

- Thanks for your contribution! Please replace this bullet-point with a description of what this PR is changing or adding and why; list any relevant issues, and review the contribution guidelines below

---

- [X] I’ve reviewed the contributor guide and applied the relevant portions to this PR.

#### Contribution guidelines:

- See our [contributor guide](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md) for general expectations for PRs.
- Larger or significant changes should be discussed in an issue before creating a PR.
- Contributions to our repos should follow the [Dart style guide](https://dart.dev/guides/language/effective-dart) and use `dart format`.
- Most changes should add an entry to the changelog and may need to [rev the pubspec package version](https://github.com/dart-lang/sdk/wiki/External-Package-Maintenance#making-a-change).
- Changes to packages require [corresponding tests](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md#Testing).

(note that many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback)

Patch:
diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md
index ab36177f38..29d7761fc6 100644
--- a/pkgs/http/CHANGELOG.md
+++ b/pkgs/http/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.0.1
+
+* Add better error messages for `SocketException`s when using `IOClient`. 
+ 
 ## 1.0.0
 
 * Requires Dart 3.0 or later.
diff --git a/pkgs/http/lib/src/client.dart b/pkgs/http/lib/src/client.dart
index 6159d0067c..9bceb887f4 100644
--- a/pkgs/http/lib/src/client.dart
+++ b/pkgs/http/lib/src/client.dart
@@ -25,6 +25,10 @@ import 'streamed_response.dart';
 /// [http.head], [http.get], [http.post], [http.put], [http.patch], or
 /// [http.delete] instead.
 ///
+/// All methods will emit a [ClientException] if there is a transport-level
+/// failure when communication with the server. For example, if the server could
+/// not be reached.
+///
 /// When creating an HTTP client class with additional functionality, you must
 /// extend [BaseClient] rather than [Client]. In most cases, you can wrap
 /// another instance of [Client] and add functionality on top of that. This
diff --git a/pkgs/http/lib/src/exception.dart b/pkgs/http/lib/src/exception.dart
index 5ac1a6441d..5d47155f30 100644
--- a/pkgs/http/lib/src/exception.dart
+++ b/pkgs/http/lib/src/exception.dart
@@ -12,5 +12,11 @@ class ClientException implements Exception {
   ClientException(this.message, [this.uri]);
 
   @override
-  String toString() => message;
+  String toString() {
+    if (uri != null) {
+      return 'ClientException: $message, uri=$uri';
+    } else {
+      return 'ClientException: $message';
+    }
+  }
 }
diff --git a/pkgs/http/lib/src/io_client.dart b/pkgs/http/lib/src/io_client.dart
index 4ebb434f9c..88d44113e8 100644
--- a/pkgs/http/lib/src/io_client.dart
+++ b/pkgs/http/lib/src/io_client.dart
@@ -6,6 +6,7 @@ import 'dart:io';
 
 import 'base_client.dart';
 import 'base_request.dart';
+import 'client.dart';
 import 'exception.dart';
 import 'io_streamed_response.dart';
 
@@ -28,9 +29,9 @@ BaseClient createClient() {
 class _ClientSocketException extends ClientException
     implements SocketException {
   final SocketException cause;
-  _ClientSocketException(SocketException e, Uri url)
+  _ClientSocketException(SocketException e, Uri uri)
       : cause = e,
-        super(e.message, url);
+        super(e.message, uri);
 
   @override
   InternetAddress? get address => cause.address;
@@ -40,9 +41,33 @@ class _ClientSocketException extends ClientException
 
   @override
   int? get port => cause.port;
+
+  @override
+  String toString() => 'ClientException with $cause, uri=$uri';
 }
 
-/// A `dart:io`-based HTTP client.
+/// A `dart:io`-based HTTP [Client].
+///
+/// If there is a socket-level failure when communicating with the server
+/// (for example, if the server could not be reached), [IOClient] will emit a
+/// [ClientException] that also implements [SocketException]. This allows
+/// callers to get more detailed exception information for socket-level
+/// failures, if desired.
+///
+/// For example:
+/// ```dart
+/// final client = http.Client();
+/// late String data;
+/// try {
+///   data = await client.read(Uri.https('example.com', ''));
+/// } on SocketException catch (e) {
+///   // Exception is transport-related, check `e.osError` for more details.
+/// } on http.ClientException catch (e) {
+///   // Exception is HTTP-related (e.g. the server returned a 404 status code).
+///   // If the handler for `SocketException` were removed then all exceptions
+///   // would be caught by this handler.
+/// }
+/// ```
 class IOClient extends BaseClient {
   /// The underlying `dart:io` HTTP client.
   HttpClient? _inner;
diff --git a/pkgs/http/pubspec.yaml b/pkgs/http/pubspec.yaml
index 08f7dd87c7..30d8878ee0 100644
--- a/pkgs/http/pubspec.yaml
+++ b/pkgs/http/pubspec.yaml
@@ -1,5 +1,5 @@
 name: http
-version: 1.0.0
+version: 1.0.1-wip
 description: A composable, multi-platform, Future-based API for HTTP requests.
 repository: https://github.com/dart-lang/http/tree/master/pkgs/http
 


Test patch:
diff --git a/pkgs/http/test/io/client_test.dart b/pkgs/http/test/io/client_test.dart
index e493931802..9476f054ad 100644
--- a/pkgs/http/test/io/client_test.dart
+++ b/pkgs/http/test/io/client_test.dart
@@ -118,7 +118,15 @@ void main() {
     request.headers[HttpHeaders.contentTypeHeader] =
         'application/json; charset=utf-8';
 
-    expect(client.send(request), throwsA(isA<SocketException>()));
+    expect(
+        client.send(request),
+        throwsA(allOf(
+            isA<http.ClientException>().having((e) => e.uri, 'uri', url),
+            isA<SocketException>().having(
+                (e) => e.toString(),
+                'SocketException.toString',
+                matches('ClientException with SocketException.*,'
+                    ' uri=http://http.invalid')))));
 
     request.sink.add('{"hello": "world"}'.codeUnits);
     request.sink.close();