From a88887aff3bb3f1d6cc8e31d9607933d440c7895 Mon Sep 17 00:00:00 2001 From: Alejandro Ulate Date: Thu, 23 Apr 2020 19:27:04 -0600 Subject: [PATCH 1/6] Changelog: - Added new utils for adding parameters to URI objects. - Added validation to check if url is either string or Uri which are the only supported types at the moment. --- example/lib/main.dart | 1 + lib/http_client_with_interceptor.dart | 8 +++- lib/models/http_interceptor_exception.dart | 12 ++++++ lib/models/models.dart | 1 + lib/models/request_data.dart | 4 +- lib/utils.dart | 50 +++++++++++++++++----- test/utils_test.dart | 37 ++++++++++++++-- 7 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 lib/models/http_interceptor_exception.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 318094b..b5c512b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -284,6 +284,7 @@ class WeatherApiInterceptor implements InterceptorContract { } catch (e) { print(e); } + print(data.params); return data; } diff --git a/lib/http_client_with_interceptor.dart b/lib/http_client_with_interceptor.dart index cd9c959..b431700 100644 --- a/lib/http_client_with_interceptor.dart +++ b/lib/http_client_with_interceptor.dart @@ -132,7 +132,13 @@ 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"); + } Request request = new Request(methodToString(method), url); if (headers != null) request.headers.addAll(headers); diff --git a/lib/models/http_interceptor_exception.dart b/lib/models/http_interceptor_exception.dart new file mode 100644 index 0000000..91b7ff1 --- /dev/null +++ b/lib/models/http_interceptor_exception.dart @@ -0,0 +1,12 @@ + + +class HttpInterceptorException implements Exception { + final message; + + HttpInterceptorException([this.message]); + + String toString() { + if (message == null) return "Exception"; + return "Exception: $message"; + } +} \ No newline at end of file diff --git a/lib/models/models.dart b/lib/models/models.dart index f03bc76..232f9c9 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -1,2 +1,3 @@ export 'request_data.dart'; export 'response_data.dart'; +export 'http_interceptor_exception.dart'; \ No newline at end of file diff --git a/lib/models/request_data.dart b/lib/models/request_data.dart index 04de9e2..f6cb4ce 100644 --- a/lib/models/request_data.dart +++ b/lib/models/request_data.dart @@ -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(); @@ -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); diff --git a/lib/utils.dart b/lib/utils.dart index 25a3cbf..70cdb3d 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -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 parameters) { +String addParametersToStringUrl(String url, Map parameters) { + return buildUrlString(url, parameters); +} + +Uri addParametersToUrl(Uri url, Map 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 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 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; } diff --git a/test/utils_test.dart b/test/utils_test.dart index 4d7032c..605e476 100644 --- a/test/utils_test.dart +++ b/test/utils_test.dart @@ -2,14 +2,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http_interceptor/utils.dart'; main() { - group("addParametersToUrl", () { + group("addParametersToStringUrl", () { test("Adds parameters to a URL string without parameters", () { // Arrange String url = "https://www.google.com/helloworld"; Map parameters = {"foo": "bar", "num": "0"}; // Act - String parameterUrl = addParametersToUrl(url, parameters); + String parameterUrl = addParametersToStringUrl(url, parameters); // Assert expect(parameterUrl, @@ -21,7 +21,7 @@ main() { Map parameters = {"extra": "1", "extra2": "anotherone"}; // Act - String parameterUrl = addParametersToUrl(url, parameters); + String parameterUrl = addParametersToStringUrl(url, parameters); // Assert expect( @@ -30,4 +30,35 @@ main() { "https://www.google.com/helloworld?foo=bar&num=0&extra=1&extra2=anotherone")); }); }); + group("addParametersToUrl", () { + test("Add parameters to Uri Url without parameters", () { + // Arrange + String stringUrl = "https://www.google.com/helloworld"; + Map parameters = {"foo": "bar", "num": "0"}; + Uri url = Uri.parse(stringUrl); + + // Act + Uri parameterUri = addParametersToUrl(url, parameters); + + // Assert + Uri expectedUrl = Uri.https("www.google.com", "/helloworld", parameters); + expect(parameterUri, equals(expectedUrl)); + }); + test("Add parameters to Uri Url with parameters", () { + // Arrange + String authority = "www.google.com"; + String unencodedPath = "/helloworld"; + Map someParameters = {"foo": "bar"}; + Map otherParameters = {"num": "0"}; + Uri url = Uri.https(authority, unencodedPath, someParameters); + + // Act + Uri parameterUri = addParametersToUrl(url, otherParameters); + + // Assert + Map allParameters = {"foo": "bar", "num": "0"}; + Uri expectedUrl = Uri.https("www.google.com", "/helloworld", allParameters); + expect(parameterUri, equals(expectedUrl)); + }); + }); } From 03cf0a82eff8d33306f5785a669155817e97e43b Mon Sep 17 00:00:00 2001 From: Alejandro Ulate Date: Thu, 23 Apr 2020 20:15:57 -0600 Subject: [PATCH 2/6] .. --- lib/http_client_with_interceptor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/http_client_with_interceptor.dart b/lib/http_client_with_interceptor.dart index b431700..7c3a220 100644 --- a/lib/http_client_with_interceptor.dart +++ b/lib/http_client_with_interceptor.dart @@ -156,7 +156,7 @@ class HttpClientWithInterceptor extends http.BaseClient { } // Intercept request - await _interceptRequest(request); + request = await _interceptRequest(request); var stream = requestTimeout == null ? await send(request) From 2fc67fcd733b06ad2b81b31678f8b4ac5b730a0f Mon Sep 17 00:00:00 2001 From: Alejandro Ulate Date: Thu, 23 Apr 2020 22:00:49 -0600 Subject: [PATCH 3/6] Changelog: - Added RetryPolicy to allow request retries after a condition is met. - Updated README to include retry policy example. --- README.md | 21 +++++++++++ lib/http_client_with_interceptor.dart | 53 ++++++++++++++++++++++----- lib/http_with_interceptor.dart | 5 +++ lib/models/models.dart | 3 +- lib/models/retry_policy.dart | 7 ++++ 5 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 lib/models/retry_policy.dart diff --git a/README.md b/README.md index 9e55386..ef4f300 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,27 @@ class WeatherRepository { } ``` +#### 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. + ### Issue Reporting Open an issue and tell me, I will be happy to help you out as soon as I can. diff --git a/lib/http_client_with_interceptor.dart b/lib/http_client_with_interceptor.dart index 7c3a220..78d06c5 100644 --- a/lib/http_client_with_interceptor.dart +++ b/lib/http_client_with_interceptor.dart @@ -37,14 +37,21 @@ import 'http_methods.dart'; class HttpClientWithInterceptor extends http.BaseClient { List 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 interceptors, Duration requestTimeout, + RetryPolicy retryPolicy, }) { assert(interceptors != null); @@ -53,6 +60,7 @@ class HttpClientWithInterceptor extends http.BaseClient { return HttpClientWithInterceptor._internal( interceptors: interceptors, requestTimeout: requestTimeout, + retryPolicy: retryPolicy, ); } @@ -137,7 +145,8 @@ class HttpClientWithInterceptor extends http.BaseClient { } else if (url is Uri) { url = addParametersToUrl(url, params); } else { - throw HttpInterceptorException("Malformed URL parameter"); + 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); @@ -155,14 +164,7 @@ class HttpClientWithInterceptor extends http.BaseClient { } } - // Intercept request - 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); @@ -180,6 +182,37 @@ class HttpClientWithInterceptor extends http.BaseClient { throw new ClientException("$message.", url); } + Future _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 _interceptRequest(Request request) async { for (InterceptorContract interceptor in interceptors) { diff --git a/lib/http_with_interceptor.dart b/lib/http_with_interceptor.dart index 19e9b55..06ddb4a 100644 --- a/lib/http_with_interceptor.dart +++ b/lib/http_with_interceptor.dart @@ -28,15 +28,18 @@ import 'package:http_interceptor/interceptor_contract.dart'; class HttpWithInterceptor { List interceptors; Duration requestTimeout; + RetryPolicy retryPolicy; HttpWithInterceptor._internal({ this.interceptors, this.requestTimeout, + this.retryPolicy, }); factory HttpWithInterceptor.build({ @required List interceptors, Duration requestTimeout, + RetryPolicy retryPolicy, }) { assert(interceptors != null); @@ -45,6 +48,7 @@ class HttpWithInterceptor { return new HttpWithInterceptor._internal( interceptors: interceptors, requestTimeout: requestTimeout, + retryPolicy: retryPolicy, ); } @@ -91,6 +95,7 @@ class HttpWithInterceptor { var client = new HttpClientWithInterceptor.build( interceptors: interceptors, requestTimeout: requestTimeout, + retryPolicy: retryPolicy, ); try { return await fn(client); diff --git a/lib/models/models.dart b/lib/models/models.dart index 232f9c9..2ef9324 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -1,3 +1,4 @@ export 'request_data.dart'; export 'response_data.dart'; -export 'http_interceptor_exception.dart'; \ No newline at end of file +export 'http_interceptor_exception.dart'; +export 'retry_policy.dart'; diff --git a/lib/models/retry_policy.dart b/lib/models/retry_policy.dart new file mode 100644 index 0000000..874a208 --- /dev/null +++ b/lib/models/retry_policy.dart @@ -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; +} \ No newline at end of file From a2e6aacd79bed952ca50d86e4adca45991c1cd41 Mon Sep 17 00:00:00 2001 From: Alejandro Ulate Date: Thu, 23 Apr 2020 22:01:25 -0600 Subject: [PATCH 4/6] Minor lint fix for README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef4f300..6788332 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ class WeatherRepository { #### 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`. +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 { From af545c7549945c5bbf630713b817585bbdd72e69 Mon Sep 17 00:00:00 2001 From: Alejandro Ulate Date: Thu, 23 Apr 2020 22:30:21 -0600 Subject: [PATCH 5/6] Adding table of contents to readme. --- README.md | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6788332..edcdacc 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,18 @@ [![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`. @@ -20,15 +25,13 @@ Include the package with the latest version available in your `pubspec.yaml`. 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. @@ -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. @@ -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. @@ -137,9 +140,9 @@ class WeatherRepository { } ``` -#### Retrying requests (NEW 🎉) +### Retrying requests -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`. + **(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 { @@ -158,6 +161,6 @@ class ExpiredTokenRetryPolicy extends RetryPolicy { 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. -### Issue Reporting +## Troubleshooting Open an issue and tell me, I will be happy to help you out as soon as I can. From abe70c55c009a5eaa925983aed371dcebf5ba091 Mon Sep 17 00:00:00 2001 From: Alejandro Ulate Date: Thu, 23 Apr 2020 22:55:37 -0600 Subject: [PATCH 6/6] Changelog: - Version bump. - Changelog updates. - pubspec.yaml updates. --- CHANGELOG.md | 5 +++++ example/pubspec.lock | 2 +- pubspec.yaml | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc6182f..3b53bf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/example/pubspec.lock b/example/pubspec.lock index de9a6e6..1ab8307 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -87,7 +87,7 @@ packages: path: ".." relative: true source: path - version: "0.2.0" + version: "0.3.0" http_parser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6524b5c..a768f66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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"