Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.3.0

* Added: RetryPolicy. It allows to attempt retries on a request when an exception occurs or when a condition from the response is met.
* Fixed: URI type urls not concatenating parameters.

## 0.2.0

* Added: Unit testing for a few of the files.
Expand Down
48 changes: 36 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,32 @@
[![codecov](https://codecov.io/gh/CodingAleCR/http_interceptor/branch/master/graph/badge.svg)](https://codecov.io/gh/CodingAleCR/http_interceptor)
[![Star on GitHub](https://img.shields.io/github/stars/codingalecr/http_interceptor.svg?style=flat&logo=github&colorB=deeppink&label=stars)](https://github.com/codingalecr/http_interceptor)

A middleware library that lets you modify requests and responses if desired. Based of on [http_middleware](https://github.com/TEDConsulting/http_middleware)
This is a plugin that lets you intercept the different requests and responses from Dart's http package. You can use to add headers, modify query params, or print a log of the response.

## Getting Started
## Quick Reference

This is a plugin that lets you intercept the different requests and responses from Dart's http package. You can use to add headers, modify query params, or print a log of the response.
- [Installation](#installation)
- [Usage](#usage)
- [Building your own interceptor](#building-your-own-interceptor)
- [Using your interceptor](#using-your-interceptor)
- [Retrying requests](#retrying-requests)
- [Having trouble? Fill an issue](#troubleshooting)

### Installing
## Installation

Include the package with the latest version available in your `pubspec.yaml`.

```dart
http_interceptor: any
```

### Importing
## Usage

```dart
import 'package:http_interceptor/http_interceptor.dart';
```

### Using `http_interceptor`

#### Building your own interceptor
### Building your own interceptor

In order to implement `http_interceptor` you need to implement the `InterceptorContract` and create your own interceptor. This abstract class has two methods: `interceptRequest`, which triggers before the http request is called; and `interceptResponse`, which triggers after the request is called, it has a response attached to it which the corresponding to said request. You could use this to do logging, adding headers, error handling, or many other cool stuff. It is important to note that after you proccess the request/response objects you need to return them so that `http` can continue the execute.

Expand Down Expand Up @@ -72,11 +75,11 @@ class WeatherApiInterceptor implements InterceptorContract {
}
```

#### Using your interceptor
### Using your interceptor

Now that you actually have your interceptor implemented, now you need to use it. There are two general ways in which you can use them: by using the `HttpWithInterceptor` to do separate connections for different requests or using a `HttpClientWithInterceptor` for keeping a connection alive while making the different `http` calls. The ideal place to use them is in the service/provider class or the repository class (if you are not using services or providers); if you don't know about the repository pattern you can just google it and you'll know what I'm talking about. ;)

##### Using interceptors with Client
#### Using interceptors with Client

Normally, this approach is taken because of its ability to be tested and mocked.

Expand Down Expand Up @@ -107,7 +110,7 @@ class WeatherRepository {
}
```

##### Using interceptors without Client
#### Using interceptors without Client

This is mostly the straight forward approach for a one-and-only call that you might need intercepted.

Expand Down Expand Up @@ -137,6 +140,27 @@ class WeatherRepository {
}
```

### Issue Reporting
### Retrying requests

**(NEW 🎉)** Sometimes you need to retry a request due to different circumstances, an expired token is a really good example. Here's how you could potentially implement an expired token retry policy with `http_interceptor`.

```dart
class ExpiredTokenRetryPolicy extends RetryPolicy {
@override
bool shouldAttemptRetryOnResponse(Response response) {
if (response.statusCode == 401) {
// Perform your token refresh here.
return true;
}
return false;
}
}
```

You can also set the maximum amount of retry attempts with `maxRetryAttempts` property or override the `shouldAttemptRetryOnException` if you want to retry the request after it failed with an exception.

## Troubleshooting

Open an issue and tell me, I will be happy to help you out as soon as I can.
1 change: 1 addition & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ class WeatherApiInterceptor implements InterceptorContract {
} catch (e) {
print(e);
}
print(data.params);
return data;
}

Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.2.0"
version: "0.3.0"
http_parser:
dependency: transitive
description:
Expand Down
59 changes: 49 additions & 10 deletions lib/http_client_with_interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,21 @@ import 'http_methods.dart';
class HttpClientWithInterceptor extends http.BaseClient {
List<InterceptorContract> interceptors;
Duration requestTimeout;
RetryPolicy retryPolicy;

final Client _client = Client();
int _retryCount = 0;

HttpClientWithInterceptor._internal({this.interceptors, this.requestTimeout});
HttpClientWithInterceptor._internal({
this.interceptors,
this.requestTimeout,
this.retryPolicy,
});

factory HttpClientWithInterceptor.build({
@required List<InterceptorContract> interceptors,
Duration requestTimeout,
RetryPolicy retryPolicy,
}) {
assert(interceptors != null);

Expand All @@ -53,6 +60,7 @@ class HttpClientWithInterceptor extends http.BaseClient {
return HttpClientWithInterceptor._internal(
interceptors: interceptors,
requestTimeout: requestTimeout,
retryPolicy: retryPolicy,
);
}

Expand Down Expand Up @@ -132,7 +140,14 @@ class HttpClientWithInterceptor extends http.BaseClient {
dynamic body,
Encoding encoding,
}) async {
if (url is String) url = Uri.parse(addParametersToUrl(url, params));
if (url is String) {
url = Uri.parse(addParametersToStringUrl(url, params));
} else if (url is Uri) {
url = addParametersToUrl(url, params);
} else {
throw HttpInterceptorException(
"Malformed URL parameter. Check that the url used is either a String or a Uri instance.");
}

Request request = new Request(methodToString(method), url);
if (headers != null) request.headers.addAll(headers);
Expand All @@ -149,14 +164,7 @@ class HttpClientWithInterceptor extends http.BaseClient {
}
}

// Intercept request
await _interceptRequest(request);

var stream = requestTimeout == null
? await send(request)
: await send(request).timeout(requestTimeout);

var response = await Response.fromStream(stream);
var response = await _attemptRequest(request);

// Intercept response
response = await _interceptResponse(response);
Expand All @@ -174,6 +182,37 @@ class HttpClientWithInterceptor extends http.BaseClient {
throw new ClientException("$message.", url);
}

Future<Response> _attemptRequest(Request request) async {
var response;
try {
// Intercept request
request = await _interceptRequest(request);

var stream = requestTimeout == null
? await send(request)
: await send(request).timeout(requestTimeout);

response = await Response.fromStream(stream);
if (retryPolicy != null
&& retryPolicy.maxRetryAttempts > _retryCount
&& retryPolicy.shouldAttemptRetryOnResponse(response)) {
_retryCount += 1;
return _attemptRequest(request);
}
} catch (error) {
if (retryPolicy != null
&& retryPolicy.maxRetryAttempts > _retryCount
&& retryPolicy.shouldAttemptRetryOnException(error)) {
_retryCount += 1;
return _attemptRequest(request);
} else {
throw HttpInterceptorException(error.toString());
}
}

return response;
}

/// This internal function intercepts the request.
Future<Request> _interceptRequest(Request request) async {
for (InterceptorContract interceptor in interceptors) {
Expand Down
5 changes: 5 additions & 0 deletions lib/http_with_interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ import 'package:http_interceptor/interceptor_contract.dart';
class HttpWithInterceptor {
List<InterceptorContract> interceptors;
Duration requestTimeout;
RetryPolicy retryPolicy;

HttpWithInterceptor._internal({
this.interceptors,
this.requestTimeout,
this.retryPolicy,
});

factory HttpWithInterceptor.build({
@required List<InterceptorContract> interceptors,
Duration requestTimeout,
RetryPolicy retryPolicy,
}) {
assert(interceptors != null);

Expand All @@ -45,6 +48,7 @@ class HttpWithInterceptor {
return new HttpWithInterceptor._internal(
interceptors: interceptors,
requestTimeout: requestTimeout,
retryPolicy: retryPolicy,
);
}

Expand Down Expand Up @@ -91,6 +95,7 @@ class HttpWithInterceptor {
var client = new HttpClientWithInterceptor.build(
interceptors: interceptors,
requestTimeout: requestTimeout,
retryPolicy: retryPolicy,
);
try {
return await fn(client);
Expand Down
12 changes: 12 additions & 0 deletions lib/models/http_interceptor_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@


class HttpInterceptorException implements Exception {
final message;

HttpInterceptorException([this.message]);

String toString() {
if (message == null) return "Exception";
return "Exception: $message";
}
}
2 changes: 2 additions & 0 deletions lib/models/models.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export 'request_data.dart';
export 'response_data.dart';
export 'http_interceptor_exception.dart';
export 'retry_policy.dart';
4 changes: 2 additions & 2 deletions lib/models/request_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class RequestData {
}) : assert(method != null),
assert(baseUrl != null);

String get url => addParametersToUrl(baseUrl, params);
String get url => addParametersToStringUrl(baseUrl, params);

factory RequestData.fromHttpRequest(Request request) {
var params = Map<String, String>();
Expand All @@ -42,7 +42,7 @@ class RequestData {
}

Request toHttpRequest() {
var reqUrl = Uri.parse(addParametersToUrl(baseUrl, params));
var reqUrl = Uri.parse(addParametersToStringUrl(baseUrl, params));

Request request = new Request(methodToString(method), reqUrl);

Expand Down
7 changes: 7 additions & 0 deletions lib/models/retry_policy.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:http/http.dart';

abstract class RetryPolicy {
bool shouldAttemptRetryOnException(Exception reason) => false;
bool shouldAttemptRetryOnResponse(Response response) => false;
final int maxRetryAttempts = 1;
}
50 changes: 40 additions & 10 deletions lib/utils.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,48 @@
/// When having an URL as String and no parameters sent then it adds
/// them to the string.
String addParametersToUrl(String url, Map<String, String> parameters) {
String addParametersToStringUrl(String url, Map<String, String> parameters) {
return buildUrlString(url, parameters);
}

Uri addParametersToUrl(Uri url, Map<String, String> parameters) {
if (parameters == null) return url;

String paramUrl = url;
if (parameters != null && parameters.length > 0) {
if (paramUrl.contains("?"))
paramUrl += "&";
else
paramUrl += "?";
String paramUrl = url.origin + url.path;

Map<String, String> newParameters = {};

url.queryParameters.forEach((key, value) {
newParameters[key] = value;
});

parameters.forEach((key, value) {
newParameters[key] = value;
});

return Uri.parse(buildUrlString(paramUrl, newParameters));
}

String buildUrlString(String url, Map<String, String> parameters) {
// Avoids unnecessary processing.
if (parameters == null) return url;

// Check if there are parameters to add.
if (parameters.length > 0) {
// Checks if the string url already has parameters.
if (url.contains("?")) {
url += "&";
} else {
url += "?";
}

// Concat every parameter to the string url.
parameters.forEach((key, value) {
paramUrl += "$key=$value&";
url += "$key=$value&";
});
paramUrl = paramUrl.substring(0, paramUrl.length - 1);

// Remove last '&' character.
url = url.substring(0, url.length - 1);
}
return paramUrl;

return url;
}
4 changes: 3 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
name: http_interceptor
description: A lightweight, simple plugin that allows you to intercept request and response objects and modify them if desired.
version: 0.2.0
version: 0.3.0
homepage: https://github.com/CodingAleCR/http_interceptor
issue_tracker: https://github.com/CodingAleCR/http_interceptor/issues
repository: https://github.com/CodingAleCR/http_interceptor

environment:
sdk: ">=2.1.0 <3.0.0"
Expand Down
Loading