intercepted_http

A composable interceptor layer for package:http.
Add auth headers, logging, token refresh, and retry logic — without replacing your HTTP client.

final client = InterceptedHttp(
  interceptors: [LoggingInterceptor(), AuthInterceptor()],
);

// Works exactly like http.Client
final response = await client.get(Uri.parse('https://api.example.com/users'));

Why

package:http is the standard Dart HTTP client, but it has no built-in way to intercept requests. Every project ends up copy-pasting the same boilerplate for auth headers, token refresh, and logging.

intercepted_http solves this once with a clean interceptor API that works for Flutter, server-side Dart, and CLI tools — no framework lock-in.

Installation

dependencies:
  intercepted_http: ^0.2.0

Quick start

import 'package:intercepted_http/intercepted_http.dart';

final client = InterceptedHttp(
  interceptors: [MyInterceptor()],
  timeout: const Duration(seconds: 30),
);

final response = await client.get(Uri.parse('https://api.example.com/todos'));
print(response.statusCode);

client.close();

Writing interceptors

Extend HttpInterceptor and override only the hooks you need. Every hook has a no-op default, so you never have to call super.

class AuthInterceptor extends HttpInterceptor {
  @override
  Future<void> onRequest(http.Request request) async {
    request.headers['Authorization'] = 'Bearer ${await getToken()}';
  }
}

Available hooks

Hook When it runs Return
onRequest Before the request is sent. Mutate headers, sign the request. void
onResponse After every response — any status code. Can transform the response. http.Response
onError Only when statusCode >= 400. Refresh tokens, fire analytics. void
shouldRetry On network exceptions or after onError. Return a Duration to retry after that delay, null to skip. Duration?

Token refresh on 401

class TokenRefreshInterceptor extends HttpInterceptor {
  bool _refreshed = false;

  @override
  Future<void> onError(http.Response response, http.Request request) async {
    if (response.statusCode == HttpStatusCode.unauthorized) {
      final newToken = await refreshToken();
      request.headers['Authorization'] = 'Bearer $newToken';
      _refreshed = true;
    }
  }

  @override
  Future<Duration?> shouldRetry(
    Object error,
    StackTrace st,
    http.Request request, {
    http.Response? response,
  }) async {
    if (response?.statusCode == HttpStatusCode.unauthorized && _refreshed) {
      _refreshed = false;
      return Duration.zero; // retry immediately with the new token
    }
    return null;
  }
}

Retry with exponential backoff

class NetworkRetryInterceptor extends HttpInterceptor {
  int _attempt = 0;

  @override
  Future<Duration?> shouldRetry(
    Object error,
    StackTrace st,
    http.Request request, {
    http.Response? response,
  }) async {
    if (response != null) return null; // don't retry HTTP errors, only exceptions
    final delay = Duration(milliseconds: 200 * (1 << _attempt));
    _attempt++;
    return delay; // 200ms, 400ms, 800ms, …
  }
}

Response transformation

onResponse returns http.Response, so interceptors can rewrite the response body. Each interceptor receives the output of the previous one, so transformations compose.

class UnwrapInterceptor extends HttpInterceptor {
  @override
  Future<http.Response> onResponse(
    http.Response response,
    http.Request request,
  ) async {
    // Unwrap {"data": {...}} envelope from every successful response
    if (response.statusCode == 200) {
      final json = jsonDecode(response.body) as Map;
      return http.Response(jsonEncode(json['data']), response.statusCode);
    }
    return response;
  }
}

Logging

class LoggingInterceptor extends HttpInterceptor {
  @override
  Future<void> onRequest(http.Request request) async {
    print('→ ${request.method} ${request.url}');
  }

  @override
  Future<http.Response> onResponse(
    http.Response response,
    http.Request request,
  ) async {
    print('← ${response.statusCode} ${request.url}');
    return response; // pass through unchanged
  }

  @override
  Future<void> onError(http.Response response, http.Request request) async {
    print('✗ ${response.statusCode} ${request.url} — ${response.body}');
  }
}

Error handling

When throwOnError: true (default), 4xx/5xx responses throw HttpClientException:

try {
  await client.get(Uri.parse('https://api.example.com/users'));
} on HttpClientException catch (e) {
  print(e.statusCode);       // 404
  print(e.message);          // extracted from {"message": "..."}
  print(e.body);             // raw response body
  print(e.data);             // decoded JSON body, if any
  print(e.isUnauthorized);   // true if 401
  print(e.isForbidden);      // true if 403
  print(e.isNotFound);       // true if 404
  print(e.isServerError);    // true if >= 500
}

Set throwOnError: false to handle errors manually via the onError hook.

HTTP status constants

Use HttpStatusCode instead of magic numbers:

if (e.statusCode == HttpStatusCode.unauthorized) { ... }
if (e.statusCode == HttpStatusCode.tooManyRequests) { ... }

All common codes from 2xx to 5xx are included.

Configuration

InterceptedHttp(
  interceptors: [...],

  // Inner client — use IOClient for mTLS, MockClient for tests
  client: IOClient(),

  // Per-request timeout (default: 30s)
  timeout: const Duration(seconds: 30),

  // Max retry attempts per request (default: 1)
  maxRetries: 3,

  // Throw HttpClientException on 4xx/5xx (default: true)
  throwOnError: true,
)

Using a custom inner client

InterceptedHttp wraps any http.Client, so you can combine it with mTLS, proxies, or mocks:

// mTLS
final client = InterceptedHttp(
  client: IOClient(mySecureHttpClient),
  interceptors: [AuthInterceptor()],
);

// Tests
final client = InterceptedHttp(
  client: MockClient((request) async => http.Response('{}', 200)),
  interceptors: [AuthInterceptor()],
);

Interceptor execution order

Interceptors run in the order they are listed:

[LoggingInterceptor, AuthInterceptor, RetryInterceptor]

  onRequest:   Logging → Auth → Retry
  onResponse:  Logging → Auth → Retry  (each can transform the response)
  onError:     Logging → Auth → Retry
  shouldRetry: Logging → Auth → Retry  (first non-null Duration wins)

License

MIT

Libraries

intercepted_http
A composable HTTP interceptor layer for package:http.