Skip to content

Commit f92167b

Browse files
committed
refactor: initial v3.0.0 implementation
1 parent 31f3d37 commit f92167b

File tree

64 files changed

+1267
-3655
lines changed

Some content is hidden

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

64 files changed

+1267
-3655
lines changed

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
build/
88

9-
109
.atom/
1110
.idea
1211
.vscode
@@ -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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
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**: Conditional export `http_interceptor_io.dart` for `IOClient` (VM/mobile/desktop; do not use on web).
11+
- **Removed**: `RequestData`/`ResponseData`. Use `BaseRequest`/`BaseResponse` only; no `copyWith` in core.
12+
- **Removed**: Dependencies `qs_dart` and `validators` (not used in v3).
13+
314
## 2.0.0
415

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

README.md

Lines changed: 42 additions & 95 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)
@@ -51,7 +51,7 @@ http_interceptor: <latest>
5151
- 🚦 Intercept & change unstreamed requests and responses.
5252
- ✨ Retrying requests when an error occurs or when the response does not match the desired (useful for handling custom error responses).
5353
- 👓 `GET` requests with separated parameters.
54-
- ⚡️ Standard `bodyBytes` on `ResponseData` to encode or decode in the desired format.
54+
- ⚡️ Standard `Response.bodyBytes` for encoding or decoding as needed.
5555
- 🙌🏼 Array parameters on requests.
5656
- 🖋 Supports self-signed certificates (except on Flutter Web).
5757
- 🍦 Compatible with vanilla Dart projects or Flutter projects.
@@ -67,114 +67,57 @@ import 'package:http_interceptor/http_interceptor.dart';
6767

6868
### Building your own interceptor
6969

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

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.
72+
- **interceptRequest** – runs before the request is sent. Return the (possibly modified) request.
73+
- **interceptResponse** – runs after the response is received. Return the (possibly modified) response.
74+
- **shouldInterceptRequest** / **shouldInterceptResponse** – return `false` to skip interception for that request/response (default `true`).
7675

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.
76+
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.
7877

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

8380
```dart
84-
class LoggerInterceptor extends InterceptorContract {
81+
class LoggerInterceptor implements HttpInterceptor {
8582
@override
86-
BaseRequest interceptRequest({
87-
required BaseRequest request,
88-
}) {
83+
BaseRequest interceptRequest({required BaseRequest request}) {
8984
print('----- Request -----');
9085
print(request.toString());
91-
print(request.headers.toString());
9286
return request;
9387
}
9488
9589
@override
96-
BaseResponse interceptResponse({
97-
required BaseResponse response,
98-
}) {
99-
log('----- Response -----');
100-
log('Code: ${response.statusCode}');
90+
BaseResponse interceptResponse({required BaseResponse response}) {
91+
print('----- Response -----');
92+
print('Code: ${response.statusCode}');
10193
if (response is Response) {
102-
log((response).body);
94+
print(response.body);
10395
}
10496
return response;
10597
}
10698
}
10799
```
108100

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` :
101+
- Adding headers / query params (in-place mutation):
146102

147103
```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-
104+
class WeatherApiInterceptor implements HttpInterceptor {
167105
@override
168-
FutureOr<bool> shouldInterceptRequest({required BaseRequest request}) async {
169-
// You can conditionally intercept requests here
170-
return true; // Intercept all requests
106+
BaseRequest interceptRequest({required BaseRequest request}) {
107+
final url = request.url.replace(
108+
queryParameters: {
109+
...request.url.queryParameters,
110+
'appid': apiKey,
111+
'units': 'metric',
112+
},
113+
);
114+
return Request(request.method, url)
115+
..headers.addAll(request.headers)
116+
..headers[HttpHeaders.contentTypeHeader] = 'application/json';
171117
}
172118
173119
@override
174-
FutureOr<bool> shouldInterceptResponse({required BaseResponse response}) async {
175-
// You can conditionally intercept responses here
176-
return true; // Intercept all responses
177-
}
120+
BaseResponse interceptResponse({required BaseResponse response}) => response;
178121
}
179122
```
180123

@@ -190,15 +133,17 @@ Here is an example with a repository using the `InterceptedClient` class.
190133

191134
```dart
192135
class WeatherRepository {
193-
Client client = InterceptedClient.build(interceptors: [
194-
WeatherApiInterceptor(),
195-
]);
136+
final client = InterceptedClient.build(
137+
interceptors: [WeatherApiInterceptor()],
138+
);
196139
197140
Future<Map<String, dynamic>> fetchCityWeather(int id) async {
198141
var parsedWeather;
199142
try {
200-
final response =
201-
await client.get("$baseUrl/weather".toUri(), params: {'id': "$id"});
143+
final response = await client.get(
144+
'$baseUrl/weather'.toUri(),
145+
params: {'id': '$id'},
146+
);
202147
if (response.statusCode == 200) {
203148
parsedWeather = json.decode(response.body);
204149
} else {
@@ -225,11 +170,13 @@ class WeatherRepository {
225170
Future<Map<String, dynamic>> fetchCityWeather(int id) async {
226171
var parsedWeather;
227172
try {
228-
final http = InterceptedHttp.build(interceptors: [
229-
WeatherApiInterceptor(),
230-
]);
231-
final response =
232-
await http.get("$baseUrl/weather".toUri(), params: {'id': "$id"});
173+
final http = InterceptedHttp.build(
174+
interceptors: [WeatherApiInterceptor()],
175+
);
176+
final response = await http.get(
177+
'$baseUrl/weather'.toUri(),
178+
params: {'id': '$id'},
179+
);
233180
if (response.statusCode == 200) {
234181
parsedWeather = json.decode(response.body);
235182
} else {

docs/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. -->

docs/decisions/001-v3-rebuild.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# V3 from-scratch rebuild
2+
3+
Date: 2025-03-15
4+
5+
Status: accepted
6+
7+
## Context
8+
9+
The library was initally a rewrite of [http_middleware](https://pub.dev/packages/http_middleware). It has reached 2.0.0 with a full feature set (interceptors, retry, timeout, copyWith on requests/responses, query params, etc.). However, the existing implementation had accumulated complexity and made a clean evolution difficult. A from-scratch rebuild was chosen to:
10+
11+
- Apply the principles, design patterns, and best practices.
12+
- Avoid inheriting code smells and anti-patterns from the previous implementation.
13+
- Prioritize a small API surface and clear behavior over backwards compatibility with 2.x.
14+
15+
The 2.0.0 API was used as a feature and API reference only; no backwards compatibility with 2.x is required.
16+
17+
## Decision
18+
19+
Rebuild the library as version 3 with the following decisions:
20+
21+
- **Decorator pattern**: The intercepted client wraps a `Client`, implements `Client`, and delegates to the inner client while adding interception. New behavior is added by composition (interceptors, retry, timeout), not by one large class.
22+
- **Strategy pattern**: Interceptors define how to transform request/response; `RetryPolicy` defines when to retry and with what delay. Both are injectable strategies.
23+
- **No copyWith in core**: Interceptors receive and return `BaseRequest`/`BaseResponse` from `package:http`. The supported pattern is in-place mutation or returning the same instance. Cloning (copyWith) was not added to the core API to keep the library simple; it can be a code smell (many clone surfaces). Importantly, `StreamedRequest` and `StreamedResponse` carry streams, which can be consumed only once—you cannot meaningfully “copy” a stream. A copyWith for those types would therefore be either limited (e.g. only URL and headers) or error-prone (e.g. reusing or re-wrapping the same stream). That constraint makes a uniform copyWith story across all request/response types fragile and reinforces the decision to avoid it in core.
24+
- **Small composable units**: Interceptor chain runner, retry executor, and timeout wrapper are separate, testable units. The client’s `send()` orchestrates them in a clear order: request interceptors → (optional) timeout → send → response interceptors; retry re-runs from request interceptors when the policy allows.
25+
- **InterceptedClient and InterceptedHttp**: `InterceptedClient` extends `BaseClient` and overrides `send()`; `InterceptedHttp` is a facade that holds an `InterceptedClient` and exposes `get`, `post`, etc., plus optional `close()`.
26+
- **Single interceptor and retry abstractions**: One `HttpInterceptor` interface and one `RetryPolicy` interface; avoid overlapping or redundant abstractions (YAGNI).
27+
28+
## Consequences
29+
30+
- **Removed**: Backwards compatibility with 2.x; `RequestData`/`ResponseData`; copyWith in the core API; any structure or naming copied from the deleted implementation.
31+
- **Added**: Clean implementation under `lib/src/` with interceptors, chain, retry, timeout, and client/facade; alignment with Decorator/Strategy and SOLID; guard clauses and linear control flow where possible.
32+
- **Preserved (conceptually)**: Interceptor contract (intercept request/response, shouldIntercept flags), retry policy (on exception and on response, configurable delay), request timeout and callback, query params (`params`/`paramsAll`) on convenience methods, Uri and String extensions where they fit the minimal API.
33+
- **Documentation**: Migration guide (e.g. [migration_guide_3.0.0.md](../../guides/migration_guide_3.0.0.md)) explains the break from 2.x and how to migrate (e.g. work with `BaseRequest`/`BaseResponse`, no copyWith).
34+

example/devtools_options.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
description: This file stores settings for Dart & Flutter DevTools.
2+
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
3+
extensions:

example/ios/Flutter/AppFrameworkInfo.plist

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,5 @@
2020
<string>????</string>
2121
<key>CFBundleVersion</key>
2222
<string>1.0</string>
23-
<key>MinimumOSVersion</key>
24-
<string>12.0</string>
2523
</dict>
2624
</plist>

example/ios/Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Uncomment this line to define a global platform for your project
2-
# platform :ios, '12.0'
2+
# platform :ios, '13.0'
33

44
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
55
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

example/ios/Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ EXTERNAL SOURCES:
2020
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
2121

2222
SPEC CHECKSUMS:
23-
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
24-
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
25-
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
23+
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
24+
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
25+
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
2626

27-
PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
27+
PODFILE CHECKSUM: 0dbd5a87e0ace00c9610d2037ac22083a01f861d
2828

2929
COCOAPODS: 1.15.2

example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@
343343
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
344344
GCC_WARN_UNUSED_FUNCTION = YES;
345345
GCC_WARN_UNUSED_VARIABLE = YES;
346-
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
346+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
347347
MTL_ENABLE_DEBUG_INFO = NO;
348348
SDKROOT = iphoneos;
349349
SUPPORTED_PLATFORMS = iphoneos;
@@ -421,7 +421,7 @@
421421
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
422422
GCC_WARN_UNUSED_FUNCTION = YES;
423423
GCC_WARN_UNUSED_VARIABLE = YES;
424-
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
424+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
425425
MTL_ENABLE_DEBUG_INFO = YES;
426426
ONLY_ACTIVE_ARCH = YES;
427427
SDKROOT = iphoneos;
@@ -470,7 +470,7 @@
470470
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
471471
GCC_WARN_UNUSED_FUNCTION = YES;
472472
GCC_WARN_UNUSED_VARIABLE = YES;
473-
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
473+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
474474
MTL_ENABLE_DEBUG_INFO = NO;
475475
SDKROOT = iphoneos;
476476
SUPPORTED_PLATFORMS = iphoneos;

0 commit comments

Comments
 (0)