Language: Português | English
Decoupled infrastructure for HTTP request management, based on contracts and semantic error handling.
Explore the documentation »
- Overview
- Requirements and Technologies
- Installation and Execution
- Folder Structure
- Architecture Components
- Execution Flow
- Resilience and Idempotency
- Advanced Features
- Implementation Examples
- License
- Author
This architecture provides a robust and decoupled HTTP layer for Flutter applications. Its goal is to ensure that all HTTP requests are handled centrally, shielding the domain and data layers from technical implementation details. By abstracting the network engine (Dio driver), the architecture allows repositories and services to receive normalized responses, eliminating the need to deal directly with raw exceptions or inconsistent behavior from external libraries.
Using an HTTP client directly within repositories or services can cause several maintenance and scalability issues:
-
Abstraction Leakage
Library-specific errors (such asDioException) reach the upper layers, making them dependent on the driver's technical details. -
Inconsistent Retries
Each part of the code might implement its own retry logic, leading to duplication and inconsistent behavior for the same type of failure. -
Incorrect Use of Idempotency
Requests may be retried indiscriminately, even in sensitive operations (likePOST), which can lead to duplicate or inconsistent data. -
Difficulty in Testing
Direct coupling with the HTTP client makes testing more complex, requiring mocks that are harder to configure. -
Lack of Visibility
Without a central control point, it becomes difficult to track the request lifecycle, such as execution time, number of attempts, failures, and success rates. This limits the ability to diagnose problems, identify performance bottlenecks, and understand application behavior.
-
HTTP Engine Isolation
Define abstract interfaces that hide the HTTP driver usage, allowing the implementation to be swapped without affecting the domain or data layers. -
Response Normalization
Standardize request returns with a single model (ApiResponse), representing both success and error consistently. -
Automated Resilience
Apply retries intelligently, considering the request type, idempotency, and the type of failure occurred. -
Semantic Error Mapping
Convert technical errors into clear categories (such asnetwork,timeout, andunauthorized), facilitating business logic handling. -
Metrics and Diagnostics
Collect information such as execution time, number of attempts, and request status to facilitate analysis. -
Support for Advanced Features
Offer integrated support for file uploads (multipart), request cancellation, and structured logs for debugging.
This architecture centralizes HTTP communication, reduces complexity in upper layers, and improves the application's reliability, consistency, and maintainability.
The architecture was developed using the following technical specifications:
- Dart SDK:
^3.10.4 - Flutter SDK:
^3.10.0or higher - Dio:
^5.9.2(Request engine) - Logger:
^2.6.2(Structured visual diagnostics) - Riverpod:
Riverpod (^3.3.1)(Dependency injection and state management)
To run the project locally, follow the steps below:
- Install project dependencies:
flutter pub get-
Ensure the Dart and Flutter environment is compatible with the versions defined in
pubspec.yaml. -
Run the project in debug mode:
flutter runlib/
└─ src/
└─ core/
└─ http/
├─ client/
│ ├─ http_client.dart
│ └─ dio_http_client.dart
├─ config/
│ └─ network_config.dart
├─ errors/
│ ├─ http_error_mapper.dart
│ ├─ http_error.dart
│ └─ http_error_type.dart
├─ executor/
│ ├─ request_executor.dart
│ └─ request_context.dart
├─ interceptors/
│ └─ logging_interceptor.dart
├─ models/
│ └─ api_response.dart
├─ multipart/
│ ├─ http_multipart.dart
│ ├─ dio_http_multipart.dart
│ └─ multipart_helper.dart
├─ options/
│ └─ http_request_options.dart
├─ tokens/
│ └─ http_cancel_token.dart
├─ types/
│ └─ progress_callback_http.dart
└─ utils/
└─ log_sanitizer.dart
Defines the HTTP client contract and its concrete implementation using Dio. Responsible for completely abstracting the network engine from the architecture.
http_client.dart→ Main HTTP client contract of the architecture.dio_http_client.dart→ Concrete implementation using Dio.
Centralizes all global network configurations for the application.
network_config.dart→ Defines baseUrl, timeouts, and default headers.
Responsible for standardizing, categorizing, and normalizing HTTP errors semantically.
http_error.dart→ Standard request error model.http_error_type.dart→ Semantic HTTP error types.http_error_mapper.dart→ Responsible for converting driver exceptions (ex: DioException) intoHttpError.
Layer responsible for executing HTTP requests with flow control, retry, and metrics.
request_executor.dart→ Orchestrates execution, retry, and resilience.request_context.dart→ Request context (time, status, retries, and metrics).
Interception layer used for logging and diagnosing the HTTP cycle.
logging_interceptor.dart→ Intercepts and records requests, responses, and errors.
Contains standardized response models for the HTTP layer.
api_response.dart→ Standard wrapper containing data, error, and context.
Responsible for abstracting file uploads via multipart/form-data.
http_multipart.dart→ Base multipart contract.dio_http_multipart.dart→ Implementation using Dio/FormData.multipart_helper.dart→ Helper for converting files to FormData.
Defines specific configurations applied per HTTP request.
http_request_options.dart→ Timeout, retry, and execution rules.
Responsible for request cancellation control.
http_cancel_token.dart→ Abstraction for canceling ongoing requests, dependent on the driver implementation (e.g., compatible withDioCancelToken, not fully agnostic).
Defines auxiliary types used in the HTTP layer.
progress_callback_http.dart→ Callback for upload/download progress.
Internal utilities for the HTTP infrastructure.
log_sanitizer.dart→ Removes/sanitizes sensitive data before logging.
The system is based on the HttpClient interface, which acts as the application's network contract. The DioHttpClient class provides the technical implementation using the Dio package, ensuring that the network engine can be replaced with minimal impact on client code.
The RequestExecutor acts as the central execution orchestrator. It manages the call flow within a retry loop, controlling parameters such as the maximum number of attempts and the wait interval between failures.
Requests return an ApiResponse<T> object instead of throwing exceptions. This container provides the typed result or an HttpError object, which classifies the failure into semantic types like network, unauthorized, or timeout.
-
Invocation: The data layer calls the
HttpClientto execute a request. -
Request Context: The system creates a
RequestContextto record the start of the operation and collect metrics like time and attempts. -
Call Execution: The
RequestExecutorperforms the request using the HTTP driver. -
Error Handling: If a failure occurs, the technical error is converted into the standard
HttpErrormodel. -
Retry (when applicable): The system checks if the error is recoverable (e.g., connection failures, timeout, HTTP 429, or 5xx errors) and if the request can be safely retried (based on the HTTP method idempotency or the
retryableconfiguration).When permitted, the
RequestExecutorapplies an exponential backoff strategy based on the configuredretryDelay. The wait time is progressively multiplied with each attempt (e.g.,baseDelay × 2ⁿ), which can result in sequences like 500ms → 1s → 2s → 4s, up to a defined maximum limit to avoid excessive delays.If
retryDelayisDuration.zero, no wait is applied between attempts.The goal of this strategy is to reduce server pressure during instability scenarios and increase the request success rate during temporary failures.
-
Result Return: The final result is encapsulated in an
ApiResponseand returned to the calling layer.
The architecture adopts technical criteria based on the HTTP standard to ensure operation integrity:
-
Idempotent Methods:
GET,PUT,DELETE,HEAD, andOPTIONSoperations are eligible for automatic retry in connection failure or server error (5xx) scenarios. -
Non-Idempotent Methods:
POSTandPATCHoperations are not automatically retried to prevent duplicate sensitive transactions, except when explicitly authorized via theretryableflag. -
Recovery Triggers: Resilience logic is triggered for failures considered temporary, such as connection errors (
connectionError), timeouts (connectionTimeout,receiveTimeout,sendTimeout), too many requests (HTTP 429), and internal server errors (status 5xx).When one of these scenarios occurs, the system checks if the request is eligible for retry based on the HTTP method idempotency or the
retryableflag. Canceled requests (cancel) are not eligible for retry.If allowed, the retry mechanism is applied, respecting the maximum number of attempts and the configured delay strategy.
Binary data management is simplified by the MultipartHelper. This utility automatically identifies and converts objects of type File, List<File>, Uint8List, or file paths (String) into MultipartFile instances. This abstraction allows upper layers to operate with native Dart types without direct dependency on Dio package specific classes.
The LoggingInterceptor provides a structured and detailed console output for monitoring the request lifecycle during development. It uses the RequestContext to calculate performance metrics and track retry counts in real-time.
Recorded at the moment the call is triggered, including the initial context and request data with masked sensitive information.
┌───────────── HTTP REQUEST ─────────────
│ ▶ REQUEST
│ POST https://api.example.com/v1/auth/login
│
│ ▶ CONTEXT
│ start=2024-10-27T10:00:00.000 | retry=0 | status=- | duration=-
│
│ ▶ HEADERS
│ {Content-Type: application/json, Accept: application/json}
│
│ ▶ BODY
│ {email: user@example.com, password: ***}
└────────────────────────────────────────
Recorded after receiving data from the server, including total execution time, final status, and masked sensitive data.
┌──────────── HTTP RESPONSE ─────────────
│ ▶ RESPONSE
│ POST https://api.example.com/v1/auth/login
│ STATUS: 200
│
│ ▶ CONTEXT
│ start=2024-10-27T10:00:00.000 | retry=0 | status=200 | duration=450ms
│
│ ▶ HEADERS
│ {content-type: [application/json; charset=utf-8], cache-control: [no-cache]}
│
│ ▶ DATA
│ {token: ***, user: {id: 1, name: User}}
└────────────────────────────────────────
Recorded in cases of technical or protocol failure, displaying the HttpError diagnosis, exception details, and stack trace.
┌───────────── HTTP ERROR ──────────────
│ ▶ REQUEST
│ GET https://api.example.com/v1/user/profile
│ STATUS: 503
│
│ ▶ CONTEXT
│ start=2024-10-27T10:05:00.000 | retry=1 | status=503 | duration=120ms
│
│ ▶ ERROR
│ HttpErrorType.server | Service Unavailable
│
│ ▶ DATA
│ {message: Server is under maintenance}
│
│ ▶ EXCEPTION
│ DioException [bad response]: The server returned an invalid response.
│
│ ▶ STACK
│ #0 RequestExecutor.execute (package:flutter_http_architecture/...)
│ #1 DioHttpClient._request (package:flutter_http_architecture/...)
└────────────────────────────────────────
final httpClient = DioHttpClient(
config: NetworkConfig(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: Duration(seconds: 15),
receiveTimeout: Duration(seconds: 15),
defaultHeaders: {
'Content-Type': 'application/json',
},
),
);final response = await httpClient.get<List<dynamic>>(
'/posts',
);
if (response.isSuccess) {
final posts = response.data;
} else {
final error = response.error;
}final response = await httpClient.get<List<dynamic>>(
'/comments',
queryParameters: {'postId': 1},
);
if (response.isSuccess) {
final comments = response.data;
} else {
final error = response.error;
}final response = await httpClient.post<Map<String, dynamic>>(
'/posts',
data: {
'title': 'foo',
'body': 'bar',
'userId': 1,
},
);
if (response.isSuccess) {
final createdPost = response.data;
} else {
final error = response.error;
}final response = await httpClient.get<List<dynamic>>(
'/posts',
options: HttpRequestOptions(
maxRetries: 2,
retryDelay: Duration(milliseconds: 300),
),
);final multipart = await MultipartHelper.fromMap({
'file': File('/path/to/file.png'),
});
final response = await httpClient.post<Map<String, dynamic>>(
'/posts',
data: multipart,
onSendProgress: (sent, total) {
final progress = sent / total;
},
);final cancelToken = DioCancelToken();
final future = httpClient.get<List<dynamic>>(
'/posts',
cancelToken: cancelToken,
);
cancelToken.cancel();
final response = await future;class PostRepository {
final HttpClient client;
PostRepository(this.client);
Future<ApiResponse<List<dynamic>?>> getPosts() {
return client.get('/posts');
}
}final response = await httpClient.get('/posts');
if (!response.isSuccess) {
switch (response.error?.type) {
case HttpErrorType.network:
break;
case HttpErrorType.timeout:
break;
case HttpErrorType.unauthorized:
break;
case HttpErrorType.server:
break;
default:
break;
}
}Distributed under the MIT License. See the LICENSE file for more information.
Developed by Dário Matias:
- Portfolio: https://dariomatias-dev.com
- GitHub: https://github.com/dariomatias-dev
- Email: matiasdario75@gmail.com
- Instagram: https://instagram.com/dariomatias_dev
- LinkedIn: https://linkedin.com/in/dariomatias-dev