Skip to content

Commit 694e34f

Browse files
authored
Initial implementation (#1)
1 parent 3a86bc8 commit 694e34f

File tree

4 files changed

+331
-0
lines changed

4 files changed

+331
-0
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Middleware for the [`http`](https://pub.dartlang.org/packages/http) package that
2+
transparently retries failing requests.
3+
4+
To use this, just create an [`RetryClient`][RetryClient] that wraps the
5+
underlying [`http.Client`][Client]:
6+
7+
[RetryClient]: https://www.dartdocs.org/documentation/http_retry/latest/http_retry/RetryClient-class.html
8+
[Client]: https://www.dartdocs.org/documentation/http/latest/http/Client-class.html
9+
10+
```dart
11+
import 'package:http/http.dart' as http;
12+
import 'package:http_retry/http_retry.dart';
13+
14+
main() async {
15+
var client = new RetryClient(new http.Client());
16+
print(await client.read("http://example.org"));
17+
await client.close();
18+
}
19+
```
20+
21+
By default, this retries any request whose response has status code 503
22+
Temporary Failure up to three retries. It waits 500ms before the first retry,
23+
and increases the delay by 1.5x each time. All of this can be customized using
24+
the [`new RetryClient()`][new RetryClient] constructor.
25+
26+
[new RetryClient]: https://www.dartdocs.org/documentation/http_retry/latest/http_retry/RetryClient/RetryClient.html

lib/http_retry.dart

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:math' as math;
7+
8+
import 'package:async/async.dart';
9+
import 'package:http/http.dart';
10+
11+
/// An HTTP client wrapper that automatically retries failing requests.
12+
class RetryClient extends BaseClient {
13+
/// The wrapped client.
14+
final Client _inner;
15+
16+
/// The number of times a request should be retried.
17+
final int _retries;
18+
19+
/// The callback that determines whether a request should be retried.
20+
final bool Function(StreamedResponse) _when;
21+
22+
/// The callback that determines how long to wait before retrying a request.
23+
final Duration Function(int) _delay;
24+
25+
/// Creates a client wrapping [inner] that retries HTTP requests.
26+
///
27+
/// This retries a failing request [retries] times (3 by default). Note that
28+
/// `n` retries means that the request will be sent at most `n + 1` times.
29+
///
30+
/// By default, this retries requests whose responses have status code 503
31+
/// Temporary Failure. If [when] is passed, it retries any request for whose
32+
/// response [when] returns `true`.
33+
///
34+
/// By default, this waits 500ms between the original request and the first
35+
/// retry, then increases the delay by 1.5x for each subsequent retry. If
36+
/// [delay] is passed, it's used to determine the time to wait before the
37+
/// given (zero-based) retry.
38+
RetryClient(this._inner,
39+
{int retries,
40+
bool when(StreamedResponse response),
41+
Duration delay(int retryCount)})
42+
: _retries = retries ?? 3,
43+
_when = when ?? ((response) => response.statusCode == 503),
44+
_delay = delay ??
45+
((retryCount) =>
46+
new Duration(milliseconds: 500) * math.pow(1.5, retryCount)) {
47+
RangeError.checkNotNegative(_retries, "retries");
48+
}
49+
50+
/// Like [new RetryClient], but with a pre-computed list of [delays]
51+
/// between each retry.
52+
///
53+
/// This will retry a request at most `delays.length` times, using each delay
54+
/// in order. It will wait for `delays[0]` after the initial request,
55+
/// `delays[1]` after the first retry, and so on.
56+
RetryClient.withDelays(Client inner, Iterable<Duration> delays,
57+
{bool when(StreamedResponse response)})
58+
: this._withDelays(inner, delays.toList(), when: when);
59+
60+
RetryClient._withDelays(Client inner, List<Duration> delays,
61+
{bool when(StreamedResponse response)})
62+
: this(inner,
63+
retries: delays.length, delay: (retryCount) => delays[retryCount]);
64+
65+
Future<StreamedResponse> send(BaseRequest request) async {
66+
var splitter = new StreamSplitter(request.finalize());
67+
68+
var i = 0;
69+
while (true) {
70+
var response = await _inner.send(_copyRequest(request, splitter.split()));
71+
if (i == _retries || !_when(response)) return response;
72+
73+
// Make sure the response stream is listened to so that we don't leave
74+
// dangling connections.
75+
response.stream.listen((_) {}).cancel()?.catchError((_) {});
76+
await new Future.delayed(_delay(i));
77+
i++;
78+
}
79+
}
80+
81+
/// Returns a copy of [original] with the given [body].
82+
StreamedRequest _copyRequest(BaseRequest original, Stream<List<int>> body) {
83+
var request = new StreamedRequest(original.method, original.url);
84+
request.contentLength = original.contentLength;
85+
request.followRedirects = original.followRedirects;
86+
request.headers.addAll(original.headers);
87+
request.maxRedirects = original.maxRedirects;
88+
request.persistentConnection = original.persistentConnection;
89+
90+
body.listen(request.sink.add,
91+
onError: request.sink.addError,
92+
onDone: request.sink.close,
93+
cancelOnError: true);
94+
95+
return request;
96+
}
97+
98+
void close() => _inner.close();
99+
}

pubspec.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: http_retry
2+
version: 0.1.0-dev
3+
description: HTTP client middleware that automatically retries requests.
4+
author: Dart Team <[email protected]>
5+
homepage: https://github.com/dart-lang/http_retry
6+
7+
environment:
8+
sdk: '>=1.24.0 <2.0.0'
9+
10+
dependencies:
11+
async: ">=1.2.0 <=3.0.0"
12+
http: "^0.11.0"
13+
14+
dev_dependencies:
15+
fake_async: "^0.1.2"
16+
test: "^0.12.0"

test/http_retry_test.dart

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:fake_async/fake_async.dart';
6+
import 'package:http/http.dart';
7+
import 'package:http/testing.dart';
8+
import 'package:test/test.dart';
9+
10+
import 'package:http_retry/http_retry.dart';
11+
12+
void main() {
13+
group("doesn't retry when", () {
14+
test("a request has a non-503 error code", () async {
15+
var client = new RetryClient(new MockClient(
16+
expectAsync1((_) async => new Response("", 502), count: 1)));
17+
var response = await client.get("http://example.org");
18+
expect(response.statusCode, equals(502));
19+
});
20+
21+
test("a request doesn't match when()", () async {
22+
var client = new RetryClient(
23+
new MockClient(
24+
expectAsync1((_) async => new Response("", 503), count: 1)),
25+
when: (_) => false);
26+
var response = await client.get("http://example.org");
27+
expect(response.statusCode, equals(503));
28+
});
29+
30+
test("retries is 0", () async {
31+
var client = new RetryClient(
32+
new MockClient(
33+
expectAsync1((_) async => new Response("", 503), count: 1)),
34+
retries: 0);
35+
var response = await client.get("http://example.org");
36+
expect(response.statusCode, equals(503));
37+
});
38+
});
39+
40+
test("retries on a 503 by default", () async {
41+
var count = 0;
42+
var client = new RetryClient(
43+
new MockClient(expectAsync1((request) async {
44+
count++;
45+
return count < 2 ? new Response("", 503) : new Response("", 200);
46+
}, count: 2)),
47+
delay: (_) => Duration.ZERO);
48+
49+
var response = await client.get("http://example.org");
50+
expect(response.statusCode, equals(200));
51+
});
52+
53+
test("retries on any request where when() returns true", () async {
54+
var count = 0;
55+
var client = new RetryClient(
56+
new MockClient(expectAsync1((request) async {
57+
count++;
58+
return new Response("", 503,
59+
headers: {"retry": count < 2 ? "true" : "false"});
60+
}, count: 2)),
61+
when: (response) => response.headers["retry"] == "true",
62+
delay: (_) => Duration.ZERO);
63+
64+
var response = await client.get("http://example.org");
65+
expect(response.headers, containsPair("retry", "false"));
66+
expect(response.statusCode, equals(503));
67+
});
68+
69+
test("retries three times by default", () async {
70+
var client = new RetryClient(
71+
new MockClient(
72+
expectAsync1((_) async => new Response("", 503), count: 4)),
73+
delay: (_) => Duration.ZERO);
74+
var response = await client.get("http://example.org");
75+
expect(response.statusCode, equals(503));
76+
});
77+
78+
test("retries the given number of times", () async {
79+
var client = new RetryClient(
80+
new MockClient(
81+
expectAsync1((_) async => new Response("", 503), count: 13)),
82+
retries: 12,
83+
delay: (_) => Duration.ZERO);
84+
var response = await client.get("http://example.org");
85+
expect(response.statusCode, equals(503));
86+
});
87+
88+
test("waits 1.5x as long each time by default", () {
89+
new FakeAsync().run((fake) {
90+
var count = 0;
91+
var client = new RetryClient(new MockClient(expectAsync1((_) async {
92+
count++;
93+
if (count == 1) {
94+
expect(fake.elapsed, equals(Duration.ZERO));
95+
} else if (count == 2) {
96+
expect(fake.elapsed, equals(new Duration(milliseconds: 500)));
97+
} else if (count == 3) {
98+
expect(fake.elapsed, equals(new Duration(milliseconds: 1250)));
99+
} else if (count == 4) {
100+
expect(fake.elapsed, equals(new Duration(milliseconds: 2375)));
101+
}
102+
103+
return new Response("", 503);
104+
}, count: 4)));
105+
106+
expect(client.get("http://example.org"), completes);
107+
fake.elapse(new Duration(minutes: 10));
108+
});
109+
});
110+
111+
test("waits according to the delay parameter", () {
112+
new FakeAsync().run((fake) {
113+
var count = 0;
114+
var client = new RetryClient(
115+
new MockClient(expectAsync1((_) async {
116+
count++;
117+
if (count == 1) {
118+
expect(fake.elapsed, equals(Duration.ZERO));
119+
} else if (count == 2) {
120+
expect(fake.elapsed, equals(Duration.ZERO));
121+
} else if (count == 3) {
122+
expect(fake.elapsed, equals(new Duration(seconds: 1)));
123+
} else if (count == 4) {
124+
expect(fake.elapsed, equals(new Duration(seconds: 3)));
125+
}
126+
127+
return new Response("", 503);
128+
}, count: 4)),
129+
delay: (requestCount) => new Duration(seconds: requestCount));
130+
131+
expect(client.get("http://example.org"), completes);
132+
fake.elapse(new Duration(minutes: 10));
133+
});
134+
});
135+
136+
test("waits according to the delay list", () {
137+
new FakeAsync().run((fake) {
138+
var count = 0;
139+
var client = new RetryClient.withDelays(
140+
new MockClient(expectAsync1((_) async {
141+
count++;
142+
if (count == 1) {
143+
expect(fake.elapsed, equals(Duration.ZERO));
144+
} else if (count == 2) {
145+
expect(fake.elapsed, equals(new Duration(seconds: 1)));
146+
} else if (count == 3) {
147+
expect(fake.elapsed, equals(new Duration(seconds: 61)));
148+
} else if (count == 4) {
149+
expect(fake.elapsed, equals(new Duration(seconds: 73)));
150+
}
151+
152+
return new Response("", 503);
153+
}, count: 4)),
154+
[
155+
new Duration(seconds: 1),
156+
new Duration(minutes: 1),
157+
new Duration(seconds: 12)
158+
]);
159+
160+
expect(client.get("http://example.org"), completes);
161+
fake.elapse(new Duration(minutes: 10));
162+
});
163+
});
164+
165+
test("copies all request attributes for each attempt", () async {
166+
var client = new RetryClient.withDelays(
167+
new MockClient(expectAsync1((request) async {
168+
expect(request.contentLength, equals(5));
169+
expect(request.followRedirects, isFalse);
170+
expect(request.headers, containsPair("foo", "bar"));
171+
expect(request.maxRedirects, equals(12));
172+
expect(request.method, equals("POST"));
173+
expect(request.persistentConnection, isFalse);
174+
expect(request.url, equals(Uri.parse("http://example.org")));
175+
expect(request.body, equals("hello"));
176+
return new Response("", 503);
177+
}, count: 2)),
178+
[Duration.ZERO]);
179+
180+
var request = new Request("POST", Uri.parse("http://example.org"));
181+
request.body = "hello";
182+
request.followRedirects = false;
183+
request.headers["foo"] = "bar";
184+
request.maxRedirects = 12;
185+
request.persistentConnection = false;
186+
187+
var response = await client.send(request);
188+
expect(response.statusCode, equals(503));
189+
});
190+
}

0 commit comments

Comments
 (0)