Initial implementation (#1)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..da41cf2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+Middleware for the [`http`](https://pub.dartlang.org/packages/http) package that
+transparently retries failing requests.
+
+To use this, just create an [`RetryClient`][RetryClient] that wraps the
+underlying [`http.Client`][Client]:
+
+[RetryClient]: https://www.dartdocs.org/documentation/http_retry/latest/http_retry/RetryClient-class.html
+[Client]: https://www.dartdocs.org/documentation/http/latest/http/Client-class.html
+
+```dart
+import 'package:http/http.dart' as http;
+import 'package:http_retry/http_retry.dart';
+
+main() async {
+  var client = new RetryClient(new http.Client());
+  print(await client.read("http://example.org"));
+  await client.close();
+}
+```
+
+By default, this retries any request whose response has status code 503
+Temporary Failure up to three retries. It waits 500ms before the first retry,
+and increases the delay by 1.5x each time. All of this can be customized using
+the [`new RetryClient()`][new RetryClient] constructor.
+
+[new RetryClient]: https://www.dartdocs.org/documentation/http_retry/latest/http_retry/RetryClient/RetryClient.html
diff --git a/lib/http_retry.dart b/lib/http_retry.dart
new file mode 100644
index 0000000..03cebd8
--- /dev/null
+++ b/lib/http_retry.dart
@@ -0,0 +1,99 @@
+// Copyright (c) 2017, 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:math' as math;
+
+import 'package:async/async.dart';
+import 'package:http/http.dart';
+
+/// An HTTP client wrapper that automatically retries failing requests.
+class RetryClient extends BaseClient {
+  /// The wrapped client.
+  final Client _inner;
+
+  /// The number of times a request should be retried.
+  final int _retries;
+
+  /// The callback that determines whether a request should be retried.
+  final bool Function(StreamedResponse) _when;
+
+  /// The callback that determines how long to wait before retrying a request.
+  final Duration Function(int) _delay;
+
+  /// Creates a client wrapping [inner] that retries HTTP requests.
+  ///
+  /// This retries a failing request [retries] times (3 by default). Note that
+  /// `n` retries means that the request will be sent at most `n + 1` times.
+  ///
+  /// 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`.
+  ///
+  /// 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
+  /// [delay] is passed, it's used to determine the time to wait before the
+  /// given (zero-based) retry.
+  RetryClient(this._inner,
+      {int retries,
+      bool when(StreamedResponse response),
+      Duration delay(int retryCount)})
+      : _retries = retries ?? 3,
+        _when = when ?? ((response) => response.statusCode == 503),
+        _delay = delay ??
+            ((retryCount) =>
+                new Duration(milliseconds: 500) * math.pow(1.5, retryCount)) {
+    RangeError.checkNotNegative(_retries, "retries");
+  }
+
+  /// Like [new RetryClient], but with a pre-computed list of [delays]
+  /// between each retry.
+  ///
+  /// This will retry a request at most `delays.length` times, using each delay
+  /// in order. It will wait for `delays[0]` after the initial request,
+  /// `delays[1]` after the first retry, and so on.
+  RetryClient.withDelays(Client inner, Iterable<Duration> delays,
+      {bool when(StreamedResponse response)})
+      : this._withDelays(inner, delays.toList(), when: when);
+
+  RetryClient._withDelays(Client inner, List<Duration> delays,
+      {bool when(StreamedResponse response)})
+      : this(inner,
+            retries: delays.length, delay: (retryCount) => delays[retryCount]);
+
+  Future<StreamedResponse> send(BaseRequest request) async {
+    var splitter = new StreamSplitter(request.finalize());
+
+    var i = 0;
+    while (true) {
+      var response = await _inner.send(_copyRequest(request, splitter.split()));
+      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));
+      i++;
+    }
+  }
+
+  /// Returns a copy of [original] with the given [body].
+  StreamedRequest _copyRequest(BaseRequest original, Stream<List<int>> body) {
+    var request = new StreamedRequest(original.method, original.url);
+    request.contentLength = original.contentLength;
+    request.followRedirects = original.followRedirects;
+    request.headers.addAll(original.headers);
+    request.maxRedirects = original.maxRedirects;
+    request.persistentConnection = original.persistentConnection;
+
+    body.listen(request.sink.add,
+        onError: request.sink.addError,
+        onDone: request.sink.close,
+        cancelOnError: true);
+
+    return request;
+  }
+
+  void close() => _inner.close();
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..6af9806
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,16 @@
+name: http_retry
+version: 0.1.0-dev
+description: HTTP client middleware that automatically retries requests.
+author: Dart Team <misc@dartlang.org>
+homepage: https://github.com/dart-lang/http_retry
+
+environment:
+  sdk: '>=1.24.0 <2.0.0'
+
+dependencies:
+  async: ">=1.2.0 <=3.0.0"
+  http: "^0.11.0"
+
+dev_dependencies:
+  fake_async: "^0.1.2"
+  test: "^0.12.0"
diff --git a/test/http_retry_test.dart b/test/http_retry_test.dart
new file mode 100644
index 0000000..3ba61b4
--- /dev/null
+++ b/test/http_retry_test.dart
@@ -0,0 +1,190 @@
+// Copyright (c) 2017, 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:fake_async/fake_async.dart';
+import 'package:http/http.dart';
+import 'package:http/testing.dart';
+import 'package:test/test.dart';
+
+import 'package:http_retry/http_retry.dart';
+
+void main() {
+  group("doesn't retry when", () {
+    test("a request has a non-503 error code", () async {
+      var client = new RetryClient(new MockClient(
+          expectAsync1((_) async => new Response("", 502), count: 1)));
+      var response = await client.get("http://example.org");
+      expect(response.statusCode, equals(502));
+    });
+
+    test("a request doesn't match when()", () async {
+      var client = new RetryClient(
+          new MockClient(
+              expectAsync1((_) async => new Response("", 503), count: 1)),
+          when: (_) => false);
+      var response = await client.get("http://example.org");
+      expect(response.statusCode, equals(503));
+    });
+
+    test("retries is 0", () async {
+      var client = new RetryClient(
+          new MockClient(
+              expectAsync1((_) async => new Response("", 503), count: 1)),
+          retries: 0);
+      var response = await client.get("http://example.org");
+      expect(response.statusCode, equals(503));
+    });
+  });
+
+  test("retries on a 503 by default", () async {
+    var count = 0;
+    var client = new RetryClient(
+        new MockClient(expectAsync1((request) async {
+          count++;
+          return count < 2 ? new Response("", 503) : new Response("", 200);
+        }, count: 2)),
+        delay: (_) => Duration.ZERO);
+
+    var response = await client.get("http://example.org");
+    expect(response.statusCode, equals(200));
+  });
+
+  test("retries on any request where when() returns true", () async {
+    var count = 0;
+    var client = new RetryClient(
+        new MockClient(expectAsync1((request) async {
+          count++;
+          return new Response("", 503,
+              headers: {"retry": count < 2 ? "true" : "false"});
+        }, count: 2)),
+        when: (response) => response.headers["retry"] == "true",
+        delay: (_) => Duration.ZERO);
+
+    var response = await client.get("http://example.org");
+    expect(response.headers, containsPair("retry", "false"));
+    expect(response.statusCode, equals(503));
+  });
+
+  test("retries three times by default", () async {
+    var client = new RetryClient(
+        new MockClient(
+            expectAsync1((_) async => new Response("", 503), count: 4)),
+        delay: (_) => Duration.ZERO);
+    var response = await client.get("http://example.org");
+    expect(response.statusCode, equals(503));
+  });
+
+  test("retries the given number of times", () async {
+    var client = new RetryClient(
+        new MockClient(
+            expectAsync1((_) async => new Response("", 503), count: 13)),
+        retries: 12,
+        delay: (_) => Duration.ZERO);
+    var response = await client.get("http://example.org");
+    expect(response.statusCode, equals(503));
+  });
+
+  test("waits 1.5x as long each time by default", () {
+    new FakeAsync().run((fake) {
+      var count = 0;
+      var client = new RetryClient(new MockClient(expectAsync1((_) async {
+        count++;
+        if (count == 1) {
+          expect(fake.elapsed, equals(Duration.ZERO));
+        } else if (count == 2) {
+          expect(fake.elapsed, equals(new Duration(milliseconds: 500)));
+        } else if (count == 3) {
+          expect(fake.elapsed, equals(new Duration(milliseconds: 1250)));
+        } else if (count == 4) {
+          expect(fake.elapsed, equals(new Duration(milliseconds: 2375)));
+        }
+
+        return new Response("", 503);
+      }, count: 4)));
+
+      expect(client.get("http://example.org"), completes);
+      fake.elapse(new Duration(minutes: 10));
+    });
+  });
+
+  test("waits according to the delay parameter", () {
+    new FakeAsync().run((fake) {
+      var count = 0;
+      var client = new RetryClient(
+          new MockClient(expectAsync1((_) async {
+            count++;
+            if (count == 1) {
+              expect(fake.elapsed, equals(Duration.ZERO));
+            } else if (count == 2) {
+              expect(fake.elapsed, equals(Duration.ZERO));
+            } else if (count == 3) {
+              expect(fake.elapsed, equals(new Duration(seconds: 1)));
+            } else if (count == 4) {
+              expect(fake.elapsed, equals(new Duration(seconds: 3)));
+            }
+
+            return new Response("", 503);
+          }, count: 4)),
+          delay: (requestCount) => new Duration(seconds: requestCount));
+
+      expect(client.get("http://example.org"), completes);
+      fake.elapse(new Duration(minutes: 10));
+    });
+  });
+
+  test("waits according to the delay list", () {
+    new FakeAsync().run((fake) {
+      var count = 0;
+      var client = new RetryClient.withDelays(
+          new MockClient(expectAsync1((_) async {
+            count++;
+            if (count == 1) {
+              expect(fake.elapsed, equals(Duration.ZERO));
+            } else if (count == 2) {
+              expect(fake.elapsed, equals(new Duration(seconds: 1)));
+            } else if (count == 3) {
+              expect(fake.elapsed, equals(new Duration(seconds: 61)));
+            } else if (count == 4) {
+              expect(fake.elapsed, equals(new Duration(seconds: 73)));
+            }
+
+            return new Response("", 503);
+          }, count: 4)),
+          [
+            new Duration(seconds: 1),
+            new Duration(minutes: 1),
+            new Duration(seconds: 12)
+          ]);
+
+      expect(client.get("http://example.org"), completes);
+      fake.elapse(new Duration(minutes: 10));
+    });
+  });
+
+  test("copies all request attributes for each attempt", () async {
+    var client = new RetryClient.withDelays(
+        new MockClient(expectAsync1((request) async {
+          expect(request.contentLength, equals(5));
+          expect(request.followRedirects, isFalse);
+          expect(request.headers, containsPair("foo", "bar"));
+          expect(request.maxRedirects, equals(12));
+          expect(request.method, equals("POST"));
+          expect(request.persistentConnection, isFalse);
+          expect(request.url, equals(Uri.parse("http://example.org")));
+          expect(request.body, equals("hello"));
+          return new Response("", 503);
+        }, count: 2)),
+        [Duration.ZERO]);
+
+    var request = new Request("POST", Uri.parse("http://example.org"));
+    request.body = "hello";
+    request.followRedirects = false;
+    request.headers["foo"] = "bar";
+    request.maxRedirects = 12;
+    request.persistentConnection = false;
+
+    var response = await client.send(request);
+    expect(response.statusCode, equals(503));
+  });
+}