HTTP Interceptors provide a powerful way to observe and modify HTTP requests and responses globally across your Revali Client. They enable cross-cutting concerns like authentication, logging, error handling, and request/response transformation.
Overview#
Interceptors run at two key points in the HTTP lifecycle:
- Before Request - Modify outgoing requests (add headers, log, etc.)
- After Response - Process incoming responses (handle errors, parse data, etc.)
Common use cases:
- Adding authentication headers
- Logging requests and responses
- Error handling and retry logic
- Request/response transformation
- Performance monitoring
- Cache management
The HttpInterceptor Interface#
Every interceptor implements the HttpInterceptor interface:
abstract interface class HttpInterceptor {
/// Called before each request is sent
FutureOr<void> onRequest(HttpRequest request);
/// Called after each response is received
FutureOr<void> onResponse(HttpResponse response);
}
Method Details#
onRequest
Called before the HTTP request is sent to the server.
Parameters:
request- TheHttpRequestobject you can inspect and modify
Common uses:
- Add or modify headers
- Log request details
- Validate request data
- Add authentication tokens
Return type: FutureOr<void> - Can be synchronous or asynchronous
onResponse
Called after the HTTP response is received from the server.
Parameters:
response- TheHttpResponseobject you can inspect and modify
Common uses:
- Log response details
- Handle authentication errors
- Transform response data
- Trigger side effects
Return type: FutureOr<void> - Can be synchronous or asynchronous
Creating an Interceptor#
Basic Interceptor#
A simple logging interceptor:
import 'dart:async';
import 'package:revali_client/revali_client.dart';
class LoggingInterceptor implements HttpInterceptor {
const LoggingInterceptor();
@override
FutureOr<void> onRequest(HttpRequest request) {
print('→ ${request.method} ${request.url}');
print(' Headers: ${request.headers}');
if (request.body.isNotEmpty) {
print(' Body: ${request.body}');
}
}
@override
FutureOr<void> onResponse(HttpResponse response) {
print('← ${response.statusCode} ${response.reasonPhrase}');
print(' Headers: ${response.headers}');
}
}
Authentication Interceptor#
Add authentication headers to every request:
class AuthInterceptor implements HttpInterceptor {
const AuthInterceptor(this.storage);
final Storage storage;
@override
Future<void> onRequest(HttpRequest request) async {
// Get token from storage
if (await storage['auth_token'] case final String token) {
// Add authorization header
request.headers['Authorization'] = 'Bearer $token';
}
}
@override
Future<void> onResponse(HttpResponse response) async {
// Handle unauthorized responses
if (response.statusCode == 401) {
// Clear auth token on unauthorized
await storage.remove('auth_token');
print('Session expired - please log in again');
}
}
}
Error Handling Interceptor#
Centralized error handling:
class ErrorInterceptor implements HttpInterceptor {
const ErrorInterceptor();
@override
void onRequest(HttpRequest request) {
// No action needed before request
}
@override
void onResponse(HttpResponse response) {
if (response.statusCode >= 400) {
// Log errors
print('Error ${response.statusCode}: ${response.reasonPhrase}');
// You could also show notifications, send to error tracking, etc.
switch (response.statusCode) {
case 400:
print('Bad Request - Check your input');
case 403:
print('Forbidden - You don\'t have permission');
case 404:
print('Not Found - Resource doesn\'t exist');
case 500:
print('Server Error - Please try again later');
}
}
}
}
Registering Interceptors#
Interceptors are registered when creating the Server instance by passing a custom HttpClient:
import 'package:revali_client/revali_client.dart';
void main() async {
// Create storage
final storage = SessionStorage();
// Create HTTP client with interceptors
final httpClient = HttpPackageClient(
interceptors: [
LoggingInterceptor(),
AuthInterceptor(storage),
ErrorInterceptor(),
],
);
// Create server with custom client
final server = Server(
client: httpClient,
storage: storage,
);
// All requests will now go through the interceptors
await server.users.getAll();
}
If you're not using a custom HttpClient, you can register the interceptors on the client instance.
final storage = SessionStorage();
final server = Server(storage: storage);
server.client.interceptors.addAll([
LoggingInterceptor(),
AuthInterceptor(storage),
ErrorInterceptor(),
]);
Interceptor Order#
Interceptors run in the order they're registered:
final httpClient = HttpPackageClient(
interceptors: [
LoggingInterceptor(), // Runs 1st for requests, 3rd for responses
AuthInterceptor(storage), // Runs 2nd for requests, 2nd for responses
ErrorInterceptor(), // Runs 3rd for requests, 1st for responses
],
);
For requests: Top to bottom
For responses: Bottom to top (reverse order)
Common Interceptor Patterns#
1. Request Headers Interceptor#
Add custom headers to all requests:
class HeadersInterceptor implements HttpInterceptor {
const HeadersInterceptor(this.headers);
final Map<String, String> headers;
@override
void onRequest(HttpRequest request) {
request.headers.addAll(headers);
}
@override
void onResponse(HttpResponse response) {}
}
// Usage
final client = HttpPackageClient(
interceptors: [
HeadersInterceptor({
'X-App-Version': '1.0.0',
'X-Platform': 'Flutter',
'Accept-Language': 'en-US',
}),
],
);
2. Performance Monitoring#
Track request timing:
class PerformanceInterceptor implements HttpInterceptor {
final Map<String, DateTime> _requestTimes = {};
@override
void onRequest(HttpRequest request) {
_requestTimes[request.url.toString()] = DateTime.now();
print('⏱️ Starting: ${request.method} ${request.url}');
}
@override
void onResponse(HttpResponse response) {
final url = response.request.url.toString();
final startTime = _requestTimes.remove(url);
if (startTime != null) {
final duration = DateTime.now().difference(startTime);
print('⏱️ Completed in ${duration.inMilliseconds}ms: $url');
if (duration.inMilliseconds > 1000) {
print('⚠️ Slow request detected!');
}
}
}
}
3. Caching Interceptor#
Cache responses for offline use:
class CacheInterceptor implements HttpInterceptor {
CacheInterceptor(this.storage);
final Storage storage;
@override
void onRequest(HttpRequest request) {
// Check cache before request (in a real implementation)
print('Checking cache for ${request.url}');
}
@override
Future<void> onResponse(HttpResponse response) async {
if (response.statusCode == 200 && response.request.method == 'GET') {
// Cache successful GET responses
final cacheKey = 'cache_${response.request.url}';
final responseData = await response.stream.join();
await storage.save(cacheKey, responseData);
print('Cached response for ${response.request.url}');
}
}
}
4. User Agent Interceptor#
Add user agent information:
class UserAgentInterceptor implements HttpInterceptor {
UserAgentInterceptor({
required this.appName,
required this.appVersion,
required this.platform,
});
final String appName;
final String appVersion;
final String platform;
@override
void onRequest(HttpRequest request) {
request.headers['User-Agent'] = '$appName/$appVersion ($platform)';
}
@override
void onResponse(HttpResponse response) {}
}
// Usage
final client = HttpPackageClient(
interceptors: [
UserAgentInterceptor(
appName: 'MyApp',
appVersion: '1.0.0',
platform: 'Android',
),
],
);
5. Debug Interceptor#
Detailed debugging information in development:
class DebugInterceptor implements HttpInterceptor {
const DebugInterceptor({this.enabled = true});
final bool enabled;
@override
void onRequest(HttpRequest request) {
if (!enabled) return;
print('╔══════════════════════════════════════════════════════════');
print('║ REQUEST');
print('╠══════════════════════════════════════════════════════════');
print('║ ${request.method} ${request.url}');
print('║ Headers:');
request.headers.forEach((key, value) {
print('║ $key: $value');
});
if (request.body.isNotEmpty) {
print('║ Body:');
print('║ ${request.body}');
}
print('╚══════════════════════════════════════════════════════════');
}
@override
void onResponse(HttpResponse response) {
if (!enabled) return;
print('╔══════════════════════════════════════════════════════════');
print('║ RESPONSE');
print('╠══════════════════════════════════════════════════════════');
print('║ ${response.statusCode} ${response.reasonPhrase}');
print('║ Headers:');
response.headers.forEach((key, value) {
print('║ $key: $value');
});
print('╚══════════════════════════════════════════════════════════');
}
}
Accessing Request and Response Data#
HttpRequest Properties#
The HttpRequest object provides:
class HttpRequest {
String method; // GET, POST, PUT, DELETE, etc.
Uri url; // Full request URL
Map<String, String> headers; // Request headers (mutable)
String body; // Request body as string
List<int>? bodyBytes; // Request body as bytes
Encoding? encoding; // Body encoding
int? contentLength; // Content length
}
Example - Modifying a request:
@override
void onRequest(HttpRequest request) {
// Add headers
request.headers['X-Custom-Header'] = 'value';
// Modify URL (though url property is final, you can inspect it)
print('Requesting: ${request.url.path}');
// Access method
if (request.method == 'POST') {
print('POST request body: ${request.body}');
}
}
HttpResponse Properties#
The HttpResponse object provides:
class HttpResponse {
HttpRequest request; // Original request
int statusCode; // HTTP status code (200, 404, etc.)
Map<String, String> headers; // Response headers
bool persistentConnection; // Keep-alive status
String? reasonPhrase; // Status text ("OK", "Not Found", etc.)
int? contentLength; // Response content length
Stream<List<int>> stream; // Response body stream
}
Example - Reading response:
@override
Future<void> onResponse(HttpResponse response) async {
// Check status
if (response.statusCode == 200) {
print('Success!');
}
// Access headers
final contentType = response.headers['content-type'];
print('Content-Type: $contentType');
// Get original request
print('Response for: ${response.request.url}');
}
What's Next?#
Now that you understand HTTP interceptors, explore these related topics:
- Storage - Manage cookies and persistent data
- Return Types - Handle custom data types
- Generated Code - Understand the client structure
- Configure - Customize client generation