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.
Already using http_interceptor
? Check out the 1.0.0 migration guide for quick reference on the changes made and how to migrate your code.
Include the package with the latest version available in your pubspec.yaml
.
http_interceptor: <latest>
- 🚦 Intercept & change unstreamed requests and responses.
- ✨ Retrying requests when an error occurs or when the response does not match the desired (useful for handling custom error responses).
- 👓
GET
requests with separated parameters. - ⚡️ Standard
bodyBytes
onResponseData
to encode or decode in the desired format. - 🙌🏼 Array parameters on requests.
- 🖋 Supports self-signed certificates (except on Flutter Web).
- 🍦 Compatible with vanilla Dart projects or Flutter projects.
- 🎉 Null-safety.
- ⏲ Timeout configuration with duration and timeout functions.
- ⏳ Configure the delay for each retry attempt.
import 'package:http_interceptor/http_interceptor.dart';
In order to implement http_interceptor
you need to implement the InterceptorContract
and create your own interceptor. This abstract class has four methods:
-
interceptRequest
, which triggers before the http request is called -
interceptResponse
, which triggers after the request is called, it has a response attached to it which the corresponding to said request; -
shouldInterceptRequest
andshouldInterceptResponse
, which are used to determine if the request or response should be intercepted or not. These two methods are optional as they returntrue
by default, but they can be useful if you want to conditionally intercept requests or responses based on certain criteria.
You could use this package 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.
All four methods use FutureOr
syntax, which makes it easier to support both synchronous and asynchronous behaviors.
- Logging with interceptor:
class LoggerInterceptor extends InterceptorContract {
@override
BaseRequest interceptRequest({
required BaseRequest request,
}) {
print('----- Request -----');
print(request.toString());
print(request.headers.toString());
return request;
}
@override
BaseResponse interceptResponse({
required BaseResponse response,
}) {
log('----- Response -----');
log('Code: ${response.statusCode}');
if (response is Response) {
log((response).body);
}
return response;
}
}
- Changing headers with interceptor:
class WeatherApiInterceptor implements InterceptorContract {
@override
FutureOr<BaseRequest> interceptRequest({required BaseRequest request}) async {
try {
request.url.queryParameters['appid'] = OPEN_WEATHER_API_KEY;
request.url.queryParameters['units'] = 'metric';
request.headers[HttpHeaders.contentTypeHeader] = "application/json";
} catch (e) {
print(e);
}
return request;
}
@override
BaseResponse interceptResponse({
required BaseResponse response,
}) =>
response;
@override
FutureOr<bool> shouldInterceptRequest({required BaseRequest request}) async {
// You can conditionally intercept requests here
return true; // Intercept all requests
}
@override
FutureOr<bool> shouldInterceptResponse({required BaseResponse response}) async {
// You can conditionally intercept responses here
return true; // Intercept all responses
}
}
- You can also react to and modify specific types of requests and responses, such as
StreamedRequest
,StreamedResponse
, orMultipartRequest
:
class MultipartRequestInterceptor implements InterceptorContract {
@override
FutureOr<BaseRequest> interceptRequest({required BaseRequest request}) async {
if(request is MultipartRequest){
request.fields['app_version'] = await PackageInfo.fromPlatform().version;
}
return request;
}
@override
FutureOr<BaseResponse> interceptResponse({required BaseResponse response}) async {
if(response is StreamedResponse){
response.stream.asBroadcastStream().listen((data){
print(data);
});
}
return response;
}
@override
FutureOr<bool> shouldInterceptRequest({required BaseRequest request}) async {
// You can conditionally intercept requests here
return true; // Intercept all requests
}
@override
FutureOr<bool> shouldInterceptResponse({required BaseResponse response}) async {
// You can conditionally intercept responses here
return true; // Intercept all responses
}
}
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 InterceptedHttp
to do separate connections for different requests or using a InterceptedClient
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. 😉
Normally, this approach is taken because of its ability to be tested and mocked.
Here is an example with a repository using the InterceptedClient
class.
class WeatherRepository {
Client client = InterceptedClient.build(interceptors: [
WeatherApiInterceptor(),
]);
Future<Map<String, dynamic>> fetchCityWeather(int id) async {
var parsedWeather;
try {
final response =
await client.get("$baseUrl/weather".toUri(), params: {'id': "$id"});
if (response.statusCode == 200) {
parsedWeather = json.decode(response.body);
} else {
throw Exception("Error while fetching. \n ${response.body}");
}
} catch (e) {
print(e);
}
return parsedWeather;
}
}
This is mostly the straight forward approach for a one-and-only call that you might need intercepted.
Here is an example with a repository using the InterceptedHttp
class.
class WeatherRepository {
Future<Map<String, dynamic>> fetchCityWeather(int id) async {
var parsedWeather;
try {
final http = InterceptedHttp.build(interceptors: [
WeatherApiInterceptor(),
]);
final response =
await http.get("$baseUrl/weather".toUri(), params: {'id': "$id"});
if (response.statusCode == 200) {
parsedWeather = json.decode(response.body);
} else {
return Future.error(
"Error while fetching.",
StackTrace.fromString("${response.body}"),
);
}
} on SocketException {
return Future.error('No Internet connection 😑');
} on FormatException {
return Future.error('Bad response format 👎');
} on Exception {
return Future.error('Unexpected error 😢');
}
return parsedWeather;
}
}
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
.
class ExpiredTokenRetryPolicy extends RetryPolicy {
@override
int get maxRetryAttempts => 2;
@override
bool shouldAttemptRetryOnException(Exception reason, BaseRequest request) {
// Log the exception for debugging
print('Request failed: ${reason.toString()}');
print('Request URL: ${request.url}');
// Retry on network exceptions, but not on client errors
return reason is SocketException || reason is TimeoutException;
}
@override
Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
if (response.statusCode == 401) {
// Perform your token refresh here.
print('Token expired, refreshing...');
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.
The RetryPolicy
abstract class provides the following methods that you can override:
shouldAttemptRetryOnException(Exception reason, BaseRequest request)
: Called when an exception occurs during the request. Returntrue
to retry,false
to fail immediately.shouldAttemptRetryOnResponse(BaseResponse response)
: Called after receiving a response. Returntrue
to retry,false
to accept the response.maxRetryAttempts
: The maximum number of retry attempts (default: 1).delayRetryAttemptOnException({required int retryAttempt})
: Delay before retrying after an exception (default: no delay).delayRetryAttemptOnResponse({required int retryAttempt})
: Delay before retrying after a response (default: no delay).
To use a retry policy, pass it to the InterceptedClient
or InterceptedHttp
:
final client = InterceptedClient.build(
interceptors: [WeatherApiInterceptor()],
retryPolicy: ExpiredTokenRetryPolicy(),
);
Sometimes it is helpful to have a cool-down phase between multiple requests. This delay could for example also differ between the first and the second retry attempt as shown in the following example.
class ExpiredTokenRetryPolicy extends RetryPolicy {
@override
int get maxRetryAttempts => 3;
@override
bool shouldAttemptRetryOnException(Exception reason, BaseRequest request) {
// Only retry on network-related exceptions
return reason is SocketException || reason is TimeoutException;
}
@override
Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
// Retry on server errors (5xx) and authentication errors (401)
return response.statusCode >= 500 || response.statusCode == 401;
}
@override
Duration delayRetryAttemptOnException({required int retryAttempt}) {
// Exponential backoff for exceptions
return Duration(milliseconds: (250 * math.pow(2.0, retryAttempt - 1)).round());
}
@override
Duration delayRetryAttemptOnResponse({required int retryAttempt}) {
// Exponential backoff for response-based retries
return Duration(milliseconds: (250 * math.pow(2.0, retryAttempt - 1)).round());
}
}
You can achieve support for self-signed certificates by providing InterceptedHttp
or InterceptedClient
with the client
parameter when using the build
method on either of those, it should look something like this:
Client client = InterceptedClient.build(
interceptors: [
WeatherApiInterceptor(),
],
client: IOClient(
HttpClient()
..badCertificateCallback = badCertificateCallback
..findProxy = findProxy,
);
);
final http = InterceptedHttp.build(
interceptors: [
WeatherApiInterceptor(),
],
client: IOClient(
HttpClient()
..badCertificateCallback = badCertificateCallback
..findProxy = findProxy,
);
);
Note: It is important to know that since both HttpClient and IOClient are part of dart:io
package, this will not be a feature that you can perform on Flutter Web (due to BrowserClient
and browser limitations).
Check out our roadmap here.
We migrated our roadmap to better suit the needs for development since we use ClickUp as our task management tool.
Open an issue and tell me, I will be happy to help you out as soon as I can.
Contributions are always welcomed and encouraged, we will always give you credit for your work on this section. If you are interested in maintaining the project on a regular basis drop me a line at me@codingale.dev.
Thanks to all the wonderful people contributing to improve this package. Check the Emoji Key for reference on what means what!