Add a whenError() callback to retry requests that error (#3)

See dart-lang/pub#1826
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ced9768..d1aa163 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.1.1
+
+* Add a `whenError()` parameter to allow requests to be retried when they
+  encounter networking errors.
+
 ## 0.1.0
 
 * Initial version.
diff --git a/lib/http_retry.dart b/lib/http_retry.dart
index 4f30e03..e1b08fd 100644
--- a/lib/http_retry.dart
+++ b/lib/http_retry.dart
@@ -19,6 +19,9 @@
   /// The callback that determines whether a request should be retried.
   final bool Function(BaseResponse) _when;
 
+  /// The callback that determines whether a request when an error is thrown.
+  final bool Function(dynamic, StackTrace) _whenError;
+
   /// The callback that determines how long to wait before retrying a request.
   final Duration Function(int) _delay;
 
@@ -32,7 +35,8 @@
   ///
   /// By default, this retries requests whose responses have status code 503
   /// Temporary Failure. If [when] is passed, it retries any request for whose
-  /// response [when] returns `true`.
+  /// response [when] returns `true`. If [whenError] is passed, it also retries
+  /// any request that throws an error for which [whenError] returns `true`.
   ///
   /// By default, this waits 500ms between the original request and the first
   /// retry, then increases the delay by 1.5x for each subsequent retry. If
@@ -40,14 +44,18 @@
   /// given (zero-based) retry.
   ///
   /// If [onRetry] is passed, it's called immediately before each retry so that
-  /// the client has a chance to perform side effects like logging.
+  /// the client has a chance to perform side effects like logging. The
+  /// `response` parameter will be null if the request was retried due to an
+  /// error for which [whenError] returned `true`.
   RetryClient(this._inner,
       {int retries,
       bool when(BaseResponse response),
+      bool whenError(error, StackTrace stackTrace),
       Duration delay(int retryCount),
       void onRetry(BaseRequest request, BaseResponse response, int retryCount)})
       : _retries = retries ?? 3,
         _when = when ?? ((response) => response.statusCode == 503),
+        _whenError = whenError ?? ((_, __) => false),
         _delay = delay ??
             ((retryCount) =>
                 new Duration(milliseconds: 500) * math.pow(1.5, retryCount)),
@@ -63,16 +71,20 @@
   /// `delays[1]` after the first retry, and so on.
   RetryClient.withDelays(Client inner, Iterable<Duration> delays,
       {bool when(BaseResponse response),
+      bool whenError(error, StackTrace stackTrace),
       void onRetry(BaseRequest request, BaseResponse response, int retryCount)})
-      : this._withDelays(inner, delays.toList(), when: when, onRetry: onRetry);
+      : this._withDelays(inner, delays.toList(),
+            when: when, whenError: whenError, onRetry: onRetry);
 
   RetryClient._withDelays(Client inner, List<Duration> delays,
       {bool when(BaseResponse response),
+      bool whenError(error, StackTrace stackTrace),
       void onRetry(BaseRequest request, BaseResponse response, int retryCount)})
       : this(inner,
             retries: delays.length,
             delay: (retryCount) => delays[retryCount],
             when: when,
+            whenError: whenError,
             onRetry: onRetry);
 
   Future<StreamedResponse> send(BaseRequest request) async {
@@ -80,12 +92,21 @@
 
     var i = 0;
     while (true) {
-      var response = await _inner.send(_copyRequest(request, splitter.split()));
-      if (i == _retries || !_when(response)) return response;
+      StreamedResponse response;
+      try {
+        response = await _inner.send(_copyRequest(request, splitter.split()));
+      } catch (error, stackTrace) {
+        if (i == _retries || !_whenError(error, stackTrace)) rethrow;
+      }
 
-      // Make sure the response stream is listened to so that we don't leave
-      // dangling connections.
-      response.stream.listen((_) {}).cancel()?.catchError((_) {});
+      if (response != null) {
+        if (i == _retries || !_when(response)) return response;
+
+        // Make sure the response stream is listened to so that we don't leave
+        // dangling connections.
+        response.stream.listen((_) {}).cancel()?.catchError((_) {});
+      }
+
       await new Future.delayed(_delay(i));
       if (_onRetry != null) _onRetry(request, response, i);
       i++;
diff --git a/pubspec.yaml b/pubspec.yaml
index b2b3c7e..030ad20 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: http_retry
-version: 0.1.0
+version: 0.1.1
 description: HTTP client middleware that automatically retries requests.
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/http_retry
diff --git a/test/http_retry_test.dart b/test/http_retry_test.dart
index a0f4d30..29ab172 100644
--- a/test/http_retry_test.dart
+++ b/test/http_retry_test.dart
@@ -66,6 +66,31 @@
     expect(response.statusCode, equals(503));
   });
 
+  test("retries on any request where whenError() returns true", () async {
+    var count = 0;
+    var client = new RetryClient(
+        new MockClient(expectAsync1((request) async {
+          count++;
+          if (count < 2) throw "oh no";
+          return new Response("", 200);
+        }, count: 2)),
+        whenError: (error, _) => error == "oh no",
+        delay: (_) => Duration.ZERO);
+
+    var response = await client.get("http://example.org");
+    expect(response.statusCode, equals(200));
+  });
+
+  test("doesn't retry a request where whenError() returns false", () async {
+    var count = 0;
+    var client = new RetryClient(
+        new MockClient(expectAsync1((request) async => throw "oh no")),
+        whenError: (error, _) => error == "oh yeah",
+        delay: (_) => Duration.ZERO);
+
+    expect(client.get("http://example.org"), throwsA("oh no"));
+  });
+
   test("retries three times by default", () async {
     var client = new RetryClient(
         new MockClient(