Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 5 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

build/


.atom/
.idea
.vscode
packages/**/ios/
packages/**/android/
doc/
doc/api/
pubspec.lock
.flutter-plugins
*.iml
Expand All @@ -21,4 +20,7 @@ coverage/
*.log
flutter_export_environment.sh
!packages/**/example/ios/
!packages/**/example/android/
!packages/**/example/android/

# FVM Version Cache
.fvm/
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 3.0.0-beta.1

- **Breaking**: Rebuild from scratch. Not backwards compatible with 2.x.
- **Added**: `HttpInterceptor` interface (replaces `InterceptorContract`). Same four methods with `FutureOr` support.
- **Added**: `InterceptedClient.build(...)` and `InterceptedHttp.build(...)` with `interceptors`, `client`, `retryPolicy`, `requestTimeout`, `onRequestTimeout`.
- **Added**: Optional `params` and `paramsAll` on `get`, `post`, `put`, `patch`, `delete`, `head`; merged into URL query.
- **Added**: `String.toUri()` extension. `Uri.addQueryParams(params: ..., paramsAll: ...)` extension.
- **Added**: `Response` JSON decoding extension (`response.jsonMap`, `response.jsonList`, `response.jsonBody`, etc.).
- **Added**: Conditional export `http_interceptor_io.dart` for `IOClient` (VM/mobile/desktop; do not use on web).
- **Removed**: `RequestData`/`ResponseData`. Use `BaseRequest`/`BaseResponse` only; no `copyWith` in core.
- **Removed**: Dependencies `qs_dart` and `validators` (not used in v3).

## 2.0.0

* feat: Simplify configuration of delay between retries by @jonasschaude in <https://github.com/CodingAleCR/http_interceptor/pull/122>
Expand Down
238 changes: 116 additions & 122 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This is a plugin that lets you intercept the different requests and responses fr

## Quick Reference

**Already using `http_interceptor`? Check out the [1.0.0 migration guide](./guides/migration_guide_1.0.0.md) for quick reference on the changes made and how to migrate your code.**
**Upgrading from 2.x? See the [3.0.0 migration guide](./guides/migration_guide_3.0.0.md).**

- [http\_interceptor](#http_interceptor)
- [Quick Reference](#quick-reference)
Expand All @@ -29,6 +29,8 @@ This is a plugin that lets you intercept the different requests and responses fr
- [Using your interceptor](#using-your-interceptor)
- [Using interceptors with Client](#using-interceptors-with-client)
- [Using interceptors without Client](#using-interceptors-without-client)
- [Working with JSON responses](#working-with-json-responses)
- [Decoding responses into models](#decoding-responses-into-models)
- [Retrying requests](#retrying-requests)
- [Using self signed certificates](#using-self-signed-certificates)
- [InterceptedClient](#interceptedclient)
Expand All @@ -51,7 +53,8 @@ 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` on `ResponseData` to encode or decode in the desired format.
- ⚡️ Standard `Response.bodyBytes` for encoding or decoding as needed.
- 📦 Convenience helpers to decode JSON responses and map them into your own models.
- 🙌🏼 Array parameters on requests.
- 🖋 Supports self-signed certificates (except on Flutter Web).
- 🍦 Compatible with vanilla Dart projects or Flutter projects.
Expand All @@ -67,114 +70,57 @@ import 'package:http_interceptor/http_interceptor.dart';

### 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 four methods:
Implement `HttpInterceptor` to add logging, headers, error handling, and more. The interface 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` and `shouldInterceptResponse`, which are used to determine if the request or response should be intercepted or not. These two methods are optional as they return `true` by default, but they can be useful if you want to conditionally intercept requests or responses based on certain criteria.
- **interceptRequest** – runs before the request is sent. Return the (possibly modified) request.
- **interceptResponse** – runs after the response is received. Return the (possibly modified) response.
- **shouldInterceptRequest** / **shouldInterceptResponse** – return `false` to skip interception for that request/response (default `true`).

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 methods support `FutureOr` so you can use sync or async. Modify the request/response in place and return it, or return a new instance.

All four methods use `FutureOr` syntax, which makes it easier to support both synchronous and asynchronous behaviors.

- Logging with interceptor:
- Logging interceptor:

```dart
class LoggerInterceptor extends InterceptorContract {
class LoggerInterceptor implements HttpInterceptor {
@override
BaseRequest interceptRequest({
required BaseRequest request,
}) {
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}');
BaseResponse interceptResponse({required BaseResponse response}) {
print('----- Response -----');
print('Code: ${response.statusCode}');
if (response is Response) {
log((response).body);
print(response.body);
}
return response;
}
}
```

- Changing headers with interceptor:

```dart
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`, or `MultipartRequest` :
- Adding headers / query params (in-place mutation):

```dart
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;
}

class WeatherApiInterceptor implements HttpInterceptor {
@override
FutureOr<bool> shouldInterceptRequest({required BaseRequest request}) async {
// You can conditionally intercept requests here
return true; // Intercept all requests
BaseRequest interceptRequest({required BaseRequest request}) {
final url = request.url.replace(
queryParameters: {
...request.url.queryParameters,
'appid': apiKey,
'units': 'metric',
},
);
return Request(request.method, url)
..headers.addAll(request.headers)
..headers[HttpHeaders.contentTypeHeader] = 'application/json';
}

@override
FutureOr<bool> shouldInterceptResponse({required BaseResponse response}) async {
// You can conditionally intercept responses here
return true; // Intercept all responses
}
BaseResponse interceptResponse({required BaseResponse response}) => response;
}
```

Expand All @@ -190,24 +136,20 @@ Here is an example with a repository using the `InterceptedClient` class.

```dart
class WeatherRepository {
Client client = InterceptedClient.build(interceptors: [
WeatherApiInterceptor(),
]);
final 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);
final response = await client.get(
'$baseUrl/weather'.toUri(),
params: {'id': '$id'},
);
if (response.statusCode == 200) {
// Built-in Response JSON helpers:
return response.jsonMap;
}
return parsedWeather;
throw Exception('Error while fetching.\\n${response.body}');
}

}
Expand All @@ -223,32 +165,84 @@ 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 😢');
final http = InterceptedHttp.build(interceptors: [WeatherApiInterceptor()]);
final response = await http.get(
'$baseUrl/weather'.toUri(),
params: {'id': '$id'},
);
if (response.statusCode == 200) {
// Built-in Response JSON helpers:
return response.jsonMap;
}
return Future.error(
'Error while fetching.',
StackTrace.fromString(response.body),
);
}

}
```

### Working with JSON responses

The `ResponseBodyDecoding` extension adds a few lightweight helpers on `Response` for common JSON use cases:

```dart
final response = await client.get(
'$baseUrl/weather'.toUri(),
params: {'id': '$id'},
);

// Dynamically-typed JSON value (Map/List/primitive or null on empty body).
final Object? json = response.jsonBody;

// JSON object as a map (throws if body is empty or not a JSON object).
final Map<String, dynamic> data = response.jsonMap;

// JSON array as a list (throws if body is empty or not a JSON array).
final List<dynamic> items = response.jsonList;
```

### Decoding responses into models

The `ResponseBodyDecoding` extension provides helpers to turn JSON responses into strongly-typed models with minimal boilerplate.

```dart
class Weather {
final String description;
final double temperature;

const Weather({
required this.description,
required this.temperature,
});

factory Weather.fromJson(Map<String, dynamic> json) {
return Weather(
description: (json['weather'] as List).first['description'] as String,
temperature: (json['main']['temp'] as num).toDouble(),
);
}
}

Future<Weather> fetchCityWeather(int id) async {
final client = InterceptedClient.build(
interceptors: [WeatherApiInterceptor()],
);

final response = await client.get(
'$baseUrl/weather'.toUri(),
params: {'id': '$id'},
);

return parsedWeather;
if (response.statusCode == 200) {
// Use the built-in JSON mapper:
return response.decodeJson(
(json) => Weather.fromJson(json as Map<String, dynamic>),
);
}

throw Exception('Error while fetching.\n${response.body}');
}
```

Expand Down
18 changes: 18 additions & 0 deletions doc/decisions/000-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Title

Date: YYYY-MM-DD

Status: proposed | rejected | accepted | deprecated | … | superseded by
[0005](0005-example.md)

## Context

<!-- Explain the overall context of the decision, the problem it attempts to solve, examples of why it might need solving. You can even add possible solutions -->

## Decision

<!-- Explain the decision that was taken. Why it was taken -->

## Consequences

<!-- Describe the consequences of this decision, deprecations, new features, removed code, etc. -->
Loading
Loading