Skip to content

Commit a9ce716

Browse files
authored
v3.0.0 (#173)
* refactor: initial v3.0.0 implementation * feat: adds json extensions for convenience and example of body parsing to models * fix: update changelog to match version
1 parent 31f3d37 commit a9ce716

71 files changed

Lines changed: 1732 additions & 3718 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66

77
build/
88

9-
109
.atom/
1110
.idea
1211
.vscode
1312
packages/**/ios/
1413
packages/**/android/
15-
doc/
14+
doc/api/
1615
pubspec.lock
1716
.flutter-plugins
1817
*.iml
@@ -21,4 +20,7 @@ coverage/
2120
*.log
2221
flutter_export_environment.sh
2322
!packages/**/example/ios/
24-
!packages/**/example/android/
23+
!packages/**/example/android/
24+
25+
# FVM Version Cache
26+
.fvm/

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

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

517
* feat: Simplify configuration of delay between retries by @jonasschaude in <https://github.com/CodingAleCR/http_interceptor/pull/122>

README.md

Lines changed: 116 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ This is a plugin that lets you intercept the different requests and responses fr
1818

1919
## Quick Reference
2020

21-
**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.**
21+
**Upgrading from 2.x? See the [3.0.0 migration guide](./guides/migration_guide_3.0.0.md).**
2222

2323
- [http\_interceptor](#http_interceptor)
2424
- [Quick Reference](#quick-reference)
@@ -29,6 +29,8 @@ This is a plugin that lets you intercept the different requests and responses fr
2929
- [Using your interceptor](#using-your-interceptor)
3030
- [Using interceptors with Client](#using-interceptors-with-client)
3131
- [Using interceptors without Client](#using-interceptors-without-client)
32+
- [Working with JSON responses](#working-with-json-responses)
33+
- [Decoding responses into models](#decoding-responses-into-models)
3234
- [Retrying requests](#retrying-requests)
3335
- [Using self signed certificates](#using-self-signed-certificates)
3436
- [InterceptedClient](#interceptedclient)
@@ -51,7 +53,8 @@ http_interceptor: <latest>
5153
- 🚦 Intercept & change unstreamed requests and responses.
5254
- ✨ Retrying requests when an error occurs or when the response does not match the desired (useful for handling custom error responses).
5355
- 👓 `GET` requests with separated parameters.
54-
- ⚡️ Standard `bodyBytes` on `ResponseData` to encode or decode in the desired format.
56+
- ⚡️ Standard `Response.bodyBytes` for encoding or decoding as needed.
57+
- 📦 Convenience helpers to decode JSON responses and map them into your own models.
5558
- 🙌🏼 Array parameters on requests.
5659
- 🖋 Supports self-signed certificates (except on Flutter Web).
5760
- 🍦 Compatible with vanilla Dart projects or Flutter projects.
@@ -67,114 +70,57 @@ import 'package:http_interceptor/http_interceptor.dart';
6770

6871
### Building your own interceptor
6972

70-
In order to implement `http_interceptor` you need to implement the `InterceptorContract` and create your own interceptor. This abstract class has four methods:
73+
Implement `HttpInterceptor` to add logging, headers, error handling, and more. The interface has four methods:
7174

72-
- `interceptRequest`, which triggers before the http request is called
73-
- `interceptResponse`, which triggers after the request is called, it has a response attached to it which the corresponding to said request;
74-
75-
- `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.
75+
- **interceptRequest** – runs before the request is sent. Return the (possibly modified) request.
76+
- **interceptResponse** – runs after the response is received. Return the (possibly modified) response.
77+
- **shouldInterceptRequest** / **shouldInterceptResponse** – return `false` to skip interception for that request/response (default `true`).
7678

77-
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.
79+
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.
7880

79-
All four methods use `FutureOr` syntax, which makes it easier to support both synchronous and asynchronous behaviors.
80-
81-
- Logging with interceptor:
81+
- Logging interceptor:
8282

8383
```dart
84-
class LoggerInterceptor extends InterceptorContract {
84+
class LoggerInterceptor implements HttpInterceptor {
8585
@override
86-
BaseRequest interceptRequest({
87-
required BaseRequest request,
88-
}) {
86+
BaseRequest interceptRequest({required BaseRequest request}) {
8987
print('----- Request -----');
9088
print(request.toString());
91-
print(request.headers.toString());
9289
return request;
9390
}
9491
9592
@override
96-
BaseResponse interceptResponse({
97-
required BaseResponse response,
98-
}) {
99-
log('----- Response -----');
100-
log('Code: ${response.statusCode}');
93+
BaseResponse interceptResponse({required BaseResponse response}) {
94+
print('----- Response -----');
95+
print('Code: ${response.statusCode}');
10196
if (response is Response) {
102-
log((response).body);
97+
print(response.body);
10398
}
10499
return response;
105100
}
106101
}
107102
```
108103

109-
- Changing headers with interceptor:
110-
111-
```dart
112-
class WeatherApiInterceptor implements InterceptorContract {
113-
@override
114-
FutureOr<BaseRequest> interceptRequest({required BaseRequest request}) async {
115-
try {
116-
request.url.queryParameters['appid'] = OPEN_WEATHER_API_KEY;
117-
request.url.queryParameters['units'] = 'metric';
118-
request.headers[HttpHeaders.contentTypeHeader] = "application/json";
119-
} catch (e) {
120-
print(e);
121-
}
122-
return request;
123-
}
124-
125-
@override
126-
BaseResponse interceptResponse({
127-
required BaseResponse response,
128-
}) =>
129-
response;
130-
131-
@override
132-
FutureOr<bool> shouldInterceptRequest({required BaseRequest request}) async {
133-
// You can conditionally intercept requests here
134-
return true; // Intercept all requests
135-
}
136-
137-
@override
138-
FutureOr<bool> shouldInterceptResponse({required BaseResponse response}) async {
139-
// You can conditionally intercept responses here
140-
return true; // Intercept all responses
141-
}
142-
}
143-
```
144-
145-
- You can also react to and modify specific types of requests and responses, such as `StreamedRequest`,`StreamedResponse`, or `MultipartRequest` :
104+
- Adding headers / query params (in-place mutation):
146105

147106
```dart
148-
class MultipartRequestInterceptor implements InterceptorContract {
149-
@override
150-
FutureOr<BaseRequest> interceptRequest({required BaseRequest request}) async {
151-
if(request is MultipartRequest){
152-
request.fields['app_version'] = await PackageInfo.fromPlatform().version;
153-
}
154-
return request;
155-
}
156-
157-
@override
158-
FutureOr<BaseResponse> interceptResponse({required BaseResponse response}) async {
159-
if(response is StreamedResponse){
160-
response.stream.asBroadcastStream().listen((data){
161-
print(data);
162-
});
163-
}
164-
return response;
165-
}
166-
107+
class WeatherApiInterceptor implements HttpInterceptor {
167108
@override
168-
FutureOr<bool> shouldInterceptRequest({required BaseRequest request}) async {
169-
// You can conditionally intercept requests here
170-
return true; // Intercept all requests
109+
BaseRequest interceptRequest({required BaseRequest request}) {
110+
final url = request.url.replace(
111+
queryParameters: {
112+
...request.url.queryParameters,
113+
'appid': apiKey,
114+
'units': 'metric',
115+
},
116+
);
117+
return Request(request.method, url)
118+
..headers.addAll(request.headers)
119+
..headers[HttpHeaders.contentTypeHeader] = 'application/json';
171120
}
172121
173122
@override
174-
FutureOr<bool> shouldInterceptResponse({required BaseResponse response}) async {
175-
// You can conditionally intercept responses here
176-
return true; // Intercept all responses
177-
}
123+
BaseResponse interceptResponse({required BaseResponse response}) => response;
178124
}
179125
```
180126

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

191137
```dart
192138
class WeatherRepository {
193-
Client client = InterceptedClient.build(interceptors: [
194-
WeatherApiInterceptor(),
195-
]);
139+
final client = InterceptedClient.build(
140+
interceptors: [WeatherApiInterceptor()],
141+
);
196142
197143
Future<Map<String, dynamic>> fetchCityWeather(int id) async {
198-
var parsedWeather;
199-
try {
200-
final response =
201-
await client.get("$baseUrl/weather".toUri(), params: {'id': "$id"});
202-
if (response.statusCode == 200) {
203-
parsedWeather = json.decode(response.body);
204-
} else {
205-
throw Exception("Error while fetching. \n ${response.body}");
206-
}
207-
} catch (e) {
208-
print(e);
144+
final response = await client.get(
145+
'$baseUrl/weather'.toUri(),
146+
params: {'id': '$id'},
147+
);
148+
if (response.statusCode == 200) {
149+
// Built-in Response JSON helpers:
150+
return response.jsonMap;
209151
}
210-
return parsedWeather;
152+
throw Exception('Error while fetching.\\n${response.body}');
211153
}
212154
213155
}
@@ -223,32 +165,84 @@ Here is an example with a repository using the `InterceptedHttp` class.
223165
class WeatherRepository {
224166
225167
Future<Map<String, dynamic>> fetchCityWeather(int id) async {
226-
var parsedWeather;
227-
try {
228-
final http = InterceptedHttp.build(interceptors: [
229-
WeatherApiInterceptor(),
230-
]);
231-
final response =
232-
await http.get("$baseUrl/weather".toUri(), params: {'id': "$id"});
233-
if (response.statusCode == 200) {
234-
parsedWeather = json.decode(response.body);
235-
} else {
236-
return Future.error(
237-
"Error while fetching.",
238-
StackTrace.fromString("${response.body}"),
239-
);
240-
}
241-
} on SocketException {
242-
return Future.error('No Internet connection 😑');
243-
} on FormatException {
244-
return Future.error('Bad response format 👎');
245-
} on Exception {
246-
return Future.error('Unexpected error 😢');
168+
final http = InterceptedHttp.build(interceptors: [WeatherApiInterceptor()]);
169+
final response = await http.get(
170+
'$baseUrl/weather'.toUri(),
171+
params: {'id': '$id'},
172+
);
173+
if (response.statusCode == 200) {
174+
// Built-in Response JSON helpers:
175+
return response.jsonMap;
247176
}
177+
return Future.error(
178+
'Error while fetching.',
179+
StackTrace.fromString(response.body),
180+
);
181+
}
182+
183+
}
184+
```
185+
186+
### Working with JSON responses
187+
188+
The `ResponseBodyDecoding` extension adds a few lightweight helpers on `Response` for common JSON use cases:
189+
190+
```dart
191+
final response = await client.get(
192+
'$baseUrl/weather'.toUri(),
193+
params: {'id': '$id'},
194+
);
195+
196+
// Dynamically-typed JSON value (Map/List/primitive or null on empty body).
197+
final Object? json = response.jsonBody;
198+
199+
// JSON object as a map (throws if body is empty or not a JSON object).
200+
final Map<String, dynamic> data = response.jsonMap;
201+
202+
// JSON array as a list (throws if body is empty or not a JSON array).
203+
final List<dynamic> items = response.jsonList;
204+
```
205+
206+
### Decoding responses into models
207+
208+
The `ResponseBodyDecoding` extension provides helpers to turn JSON responses into strongly-typed models with minimal boilerplate.
209+
210+
```dart
211+
class Weather {
212+
final String description;
213+
final double temperature;
214+
215+
const Weather({
216+
required this.description,
217+
required this.temperature,
218+
});
219+
220+
factory Weather.fromJson(Map<String, dynamic> json) {
221+
return Weather(
222+
description: (json['weather'] as List).first['description'] as String,
223+
temperature: (json['main']['temp'] as num).toDouble(),
224+
);
225+
}
226+
}
227+
228+
Future<Weather> fetchCityWeather(int id) async {
229+
final client = InterceptedClient.build(
230+
interceptors: [WeatherApiInterceptor()],
231+
);
232+
233+
final response = await client.get(
234+
'$baseUrl/weather'.toUri(),
235+
params: {'id': '$id'},
236+
);
248237
249-
return parsedWeather;
238+
if (response.statusCode == 200) {
239+
// Use the built-in JSON mapper:
240+
return response.decodeJson(
241+
(json) => Weather.fromJson(json as Map<String, dynamic>),
242+
);
250243
}
251244
245+
throw Exception('Error while fetching.\n${response.body}');
252246
}
253247
```
254248

doc/decisions/000-template.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Title
2+
3+
Date: YYYY-MM-DD
4+
5+
Status: proposed | rejected | accepted | deprecated | … | superseded by
6+
[0005](0005-example.md)
7+
8+
## Context
9+
10+
<!-- 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 -->
11+
12+
## Decision
13+
14+
<!-- Explain the decision that was taken. Why it was taken -->
15+
16+
## Consequences
17+
18+
<!-- Describe the consequences of this decision, deprecations, new features, removed code, etc. -->

0 commit comments

Comments
 (0)