davianspace_http_resilience 1.0.3 copy "davianspace_http_resilience: ^1.0.3" to clipboard
davianspace_http_resilience: ^1.0.3 copied to clipboard

Production-grade HTTP resilience for Dart & Flutter. Retry, circuit breaker, timeout, bulkhead, hedging, fallback, structured observability, and header-redacted logging.

example/example.dart

// ignore_for_file: avoid_print
import 'dart:convert';

import 'package:davianspace_http_resilience/davianspace_http_resilience.dart';
import 'package:davianspace_logging/davianspace_logging.dart';

/// ============================================================
/// davianspace_http_resilience — Production Usage Examples
/// ============================================================
///
/// This example demonstrates enterprise-ready patterns:
///
///  1. Full-stack resilient client with all policies
///  2. Named client factory registry
///  3. Custom DelegatingHandler (auth / correlation)
///  4. CancellationToken usage
///  5. RetryPredicates composition
///  6. Configuration-driven policies (JSON)
///  7. Per-request streaming override
///  8. Circuit breaker health monitoring
///  9. Header-redacted security logging
///  10. Rate limiter integration (davianspace_http_ratelimit companion)
///
/// Run with:
///   dart run example/example.dart
/// ============================================================

// Module-level factory instance for Example 2 (named-client registry pattern).
final _factory = HttpClientFactory();
// Module-level logger factory for structured HTTP logging.
late LoggerFactory _logFactory;

void main() async {
  _configureLogging();

  await _example1BasicClient();
  await _example2NamedClientFactory();
  await _example3CustomHandler();
  _example4CancellationToken();
  _example5RetryPredicates();
  _example6ConfigDrivenPolicies();
  _example7PerRequestStreaming();
  _example8CircuitBreakerHealth();
  _example9HeaderRedaction();
  _example10RateLimiterIntegration();
  print('\nAll examples completed.');
}

// ─────────────────────────────────────────────────────────────
// 1. Full-stack resilient client
// ─────────────────────────────────────────────────────────────

Future<void> _example1BasicClient() async {
  print('\n[Example 1] Resilient client with full policy stack');

  final client = HttpClientBuilder('jsonplaceholder')
      .withBaseUri(Uri.parse('https://jsonplaceholder.typicode.com'))
      .withDefaultHeader('Accept', 'application/json')
      .withLogging(logger: _logFactory.createLogger('davianspace.http'))
      .withRetry(
        RetryPolicy.exponential(
          maxRetries: 3,
          baseDelay: const Duration(milliseconds: 100),
          useJitter: true,
          shouldRetry:
              RetryPredicates.serverErrors.or(RetryPredicates.networkErrors),
        ),
      )
      .withCircuitBreaker(
        const CircuitBreakerPolicy(
          circuitName: 'jsonplaceholder',
        ),
      )
      .withTimeout(
        const TimeoutPolicy(timeout: Duration(seconds: 10)),
      )
      .withBulkhead(
        const BulkheadPolicy(maxQueueDepth: 50),
      )
      .build();

  try {
    final response = await client
        .get(Uri.parse('/todos/1'))
        .then((HttpResponse r) => r.ensureSuccess());

    final json = response.bodyAsJsonMap;
    print('  → title: ${json?['title']}');
    print('  → duration: ${response.duration.inMilliseconds}ms');
  } on HttpStatusException catch (e) {
    print('  ✗ HTTP error: $e');
  } on HttpResilienceException catch (e) {
    print('  ✗ Resilience error: $e');
  } finally {
    client.dispose();
  }
}

// ─────────────────────────────────────────────────────────────
// 2. Named client factory registry
// ─────────────────────────────────────────────────────────────

Future<void> _example2NamedClientFactory() async {
  print('\n[Example 2] Named client factory');

  // Register once (e.g. in main() or a DI bootstrapper).
  if (!_factory.hasClient('posts-api')) {
    _factory.addClient(
      'posts-api',
      (b) => b
          .withBaseUri(Uri.parse('https://jsonplaceholder.typicode.com'))
          .withRetry(RetryPolicy.constant(maxRetries: 2))
          .withLogging(logger: _logFactory.createLogger('davianspace.http')),
    );
  }

  // Resolve anywhere in the application.
  final client = _factory.createClient('posts-api');

  try {
    final response = await client.get(Uri.parse('/posts/1'));
    print('  → status: ${response.statusCode}');
    final map = response.bodyAsJsonMap;
    print('  → id: ${map?["id"]}, userId: ${map?["userId"]}');
  } on Exception catch (e) {
    print('  ✗ $e');
  } finally {
    _factory.clear(); // cleanup between examples
  }
}

// ─────────────────────────────────────────────────────────────
// 3. Custom handler (correlation ID injection)
// ─────────────────────────────────────────────────────────────

Future<void> _example3CustomHandler() async {
  print('\n[Example 3] Custom DelegatingHandler in the pipeline');

  final client = HttpClientBuilder('custom')
      .withBaseUri(Uri.parse('https://jsonplaceholder.typicode.com'))
      .addHandler(_CorrelationIdHandler())
      .withLogging(logger: _logFactory.createLogger('davianspace.http'))
      .build();

  try {
    final response = await client.get(Uri.parse('/users/1'));
    print(
      '  → X-Correlation-Id echoed: '
      '${response.headers['x-correlation-id'] ?? "(not echoed by server — check request)"}',
    );
  } finally {
    client.dispose();
  }
}

// ─────────────────────────────────────────────────────────────
// 4. CancellationToken
// ─────────────────────────────────────────────────────────────

void _example4CancellationToken() {
  print('\n[Example 4] CancellationToken');

  final token = CancellationToken();
  print('  isCancelled before: ${token.isCancelled}');
  token.cancel('user pressed back');
  print('  isCancelled after:  ${token.isCancelled}');
  print('  reason: ${token.reason}');

  try {
    token.throwIfCancelled();
  } on CancellationException catch (e) {
    print('  Caught: $e');
  }
}

// ─────────────────────────────────────────────────────────────
// 5. RetryPredicates composition
// ─────────────────────────────────────────────────────────────

void _example5RetryPredicates() {
  print('\n[Example 5] RetryPredicates composition');

  final predicate = RetryPredicates.serverErrors
      .or(RetryPredicates.rateLimitAndServiceUnavailable);

  final ctx = HttpContext(
    request: HttpRequest(
      method: HttpMethod.get,
      uri: Uri.parse('https://example.com'),
    ),
  );

  print(
    '  Should retry 503: '
    '${predicate(HttpResponse(statusCode: 503), null, ctx)}',
  );
  print(
    '  Should retry 429: '
    '${predicate(HttpResponse(statusCode: 429), null, ctx)}',
  );
  print(
    '  Should retry 404: '
    '${predicate(HttpResponse(statusCode: 404), null, ctx)}',
  );
  print(
    '  Should retry exception: '
    '${predicate(null, Exception('network'), ctx)}',
  );
}

// ─────────────────────────────────────────────────────────────
// 6. Configuration-driven policies (JSON)
// ─────────────────────────────────────────────────────────────

void _example6ConfigDrivenPolicies() {
  print('\n[Example 6] Configuration-driven policies from JSON');

  const configJson = '''
  {
    "Resilience": {
      "Retry": {
        "MaxRetries": 3,
        "Backoff": { "Type": "exponential", "BaseMs": 200, "UseJitter": true }
      },
      "Timeout": { "Seconds": 10 },
      "CircuitBreaker": {
        "CircuitName": "config-demo",
        "FailureThreshold": 5,
        "BreakSeconds": 30
      },
      "BulkheadIsolation": {
        "MaxConcurrentRequests": 20,
        "MaxQueueSize": 50
      },
      "Hedging": {
        "HedgeAfterMs": 300,
        "MaxHedgedAttempts": 2
      },
      "Fallback": {
        "StatusCodes": [500, 502, 503, 504]
      }
    }
  }
  ''';

  const loader = ResilienceConfigLoader();
  const binder = ResilienceConfigBinder();

  final config = loader.load(configJson);
  print('  → Config loaded: isEmpty=${config.isEmpty}');
  print('  → Retry: ${config.retry}');
  print('  → Timeout: ${config.timeout}');
  print('  → CircuitBreaker: ${config.circuitBreaker}');
  print('  → BulkheadIsolation: ${config.bulkheadIsolation}');
  print('  → Hedging: ${config.hedging}');
  print('  → Fallback: ${config.fallback}');

  // Build a composed pipeline from all configured sections
  final pipeline = binder.buildPipeline(config);
  print('  → Pipeline built: $pipeline');

  // Build individual policies
  if (config.hedging != null) {
    final hedging = binder.buildHedging(config.hedging!);
    print('  → Hedging policy: $hedging');
  }

  if (config.fallback != null) {
    final fallback = binder.buildFallbackPolicy(
      config.fallback!,
      fallbackAction: (ctx, err, st) async => HttpResponse(
        statusCode: 200,
        body: utf8.encode('{"offline": true}'),
      ),
    );
    print('  → Fallback policy: $fallback');
  }

  // Register all into the policy registry
  PolicyRegistry.instance.loadFromConfig(config, prefix: 'demo');
  print('  → Policies registered in PolicyRegistry with prefix "demo"');

  // Cleanup
  pipeline.dispose();
}

// ─────────────────────────────────────────────────────────────
// 7. Per-request streaming override
// ─────────────────────────────────────────────────────────────

void _example7PerRequestStreaming() {
  print('\n[Example 7] Per-request streaming override');

  // Demonstrate the metadata key (no actual HTTP call needed)
  final request = HttpRequest(
    method: HttpMethod.get,
    uri: Uri.parse('https://api.example.com/large-file'),
    metadata: {HttpRequest.streamingKey: true},
  );

  final isStreaming =
      request.metadata[HttpRequest.streamingKey] as bool? ?? false;
  print('  → Metadata key: ${HttpRequest.streamingKey}');
  print('  → Request streaming override: $isStreaming');

  // Force-buffer example
  final bufferedRequest = HttpRequest(
    method: HttpMethod.get,
    uri: Uri.parse('https://api.example.com/small-resource'),
    metadata: {HttpRequest.streamingKey: false},
  );

  final isBuffered =
      bufferedRequest.metadata[HttpRequest.streamingKey] as bool? ?? true;
  print('  → Force-buffer override: ${!isBuffered}');
}

// ─────────────────────────────────────────────────────────────
// 8. Circuit breaker health monitoring
// ─────────────────────────────────────────────────────────────

void _example8CircuitBreakerHealth() {
  print('\n[Example 8] Circuit breaker health monitoring');

  final registry = CircuitBreakerRegistry.instance;

  // Check if we have circuits from earlier examples
  final names = registry.circuitNames.toList();
  print('  → Registered circuits: $names');

  // registry.isHealthy checks ALL circuits (getter, not a method).
  print('  → All circuits healthy: ${registry.isHealthy}');

  // Use snapshot for per-circuit state details.
  final snap = registry.snapshot;
  for (final entry in snap.entries) {
    print('  → Circuit "${entry.key}": state=${entry.value}');
  }

  if (names.isEmpty) {
    print('  → (No circuits registered — run Example 1 first)');
  }
}

// ─────────────────────────────────────────────────────────────
// 9. Header-redacted security logging
// ─────────────────────────────────────────────────────────────

void _example9HeaderRedaction() {
  print('\n[Example 9] Header redaction in structured logging');

  // Build a LoggingHandler that emits structured JSON and redacts sensitive
  // headers.  The default redaction set covers Authorization, Cookie,
  // Set-Cookie, Proxy-Authorization, and X-Api-Key.  Supply a custom set to
  // add or narrow that list.
  final defaultHandler = LoggingHandler(
    structured: true,
    logHeaders: true,
    // Default redaction set: authorization, cookie, set-cookie,
    // proxy-authorization, x-api-key.
  );

  final restrictedHandler = LoggingHandler(
    structured: true,
    logHeaders: true,
    // Organisation-specific additions on top of the defaults.
    redactedHeaders: const {
      'authorization',
      'cookie',
      'x-api-key',
      'x-internal-token',
      'x-impersonation-user',
    },
    uriSanitizer: (uri) {
      // Strip both query params and the fragment before logging.
      return uri.replace(queryParameters: const {}, fragment: '').toString();
    },
  );

  // Integrate into the builder pipeline with addHandler().
  final client = HttpClientBuilder()
      .withBaseUri(Uri.parse('https://api.example.com'))
      .addHandler(defaultHandler)
      .build();

  final restrictedClient = HttpClientBuilder()
      .withBaseUri(Uri.parse('https://internal.api.example.com'))
      .addHandler(restrictedHandler)
      .build();

  print('  Default handler — redacts: authorization, cookie, set-cookie,'
      ' proxy-authorization, x-api-key');
  print('  Default client ready    : ${client.runtimeType}');
  print('  Restricted client ready : ${restrictedClient.runtimeType}');

  client.dispose();
  restrictedClient.dispose();
}

// ─────────────────────────────────────────────────────────────
// 10. Rate limiter integration (davianspace_http_ratelimit companion)
// ─────────────────────────────────────────────────────────────

/// Shows how to add client-side rate limiting to any [HttpClientBuilder]
/// pipeline using the companion package.
///
/// The companion package ships a `HttpClientBuilderRateLimitExtension` that
/// adds `.withRateLimit(RateLimitPolicy)` directly to [HttpClientBuilder],
/// so all six rate-limiting algorithms slot seamlessly into the fluent API.
void _example10RateLimiterIntegration() {
  print('\n[Example 10] Rate limiter integration');

  // This example shows the resilience-only pipeline that anchors into a full
  // retry + circuit-breaker chain.  Add davianspace_http_ratelimit to pubspec
  // and call .withRateLimit(policy) between .withLogging() and .withRetry()
  // to cap outbound throughput before resilience policies fire.
  //
  // pubspec.yaml:
  //   dependencies:
  //     davianspace_http_resilience: ^1.0.2
  //     davianspace_http_ratelimit:  ^1.0.0
  //
  // Full pipeline with rate limiting (token bucket — 100 req/s, burst 200):
  //
  //   final client = HttpClientBuilder('my-api')
  //       .withBaseUri(Uri.parse('https://api.example.com'))
  //       .withLogging()
  //       .withRateLimit(RateLimitPolicy(        // ← companion extension
  //         limiter: TokenBucketRateLimiter(
  //           capacity: 200,
  //           refillAmount: 100,
  //           refillInterval: Duration(seconds: 1),
  //         ),
  //         acquireTimeout: Duration(milliseconds: 500),
  //         respectServerHeaders: true,
  //         onRejected: (ctx, e) => log.warning('rate-limited: $e'),
  //       ))
  //       .withRetry(RetryPolicy.exponential(maxRetries: 3))
  //       .withCircuitBreaker(const CircuitBreakerPolicy(circuitName: 'my-api'))
  //       .build();
  //
  // Policy order matters: logging captures the full picture first, rate
  // limiting gates throughput, then retry handles transient failures, and
  // the circuit breaker trips on sustained error rates.

  // Build the resilience-only portion to confirm it compiles independently.
  final baseClient = HttpClientBuilder()
      .withBaseUri(Uri.parse('https://api.example.com'))
      .withLogging(logger: _logFactory.createLogger('davianspace.http'))
      .withRetry(RetryPolicy.exponential(maxRetries: 3))
      .withCircuitBreaker(const CircuitBreakerPolicy(circuitName: 'my-api'))
      .build();

  print('  Resilience-only pipeline built: ${baseClient.runtimeType}');
  print('  Add davianspace_http_ratelimit and insert .withRateLimit() after'
      ' .withLogging() to gate outbound throughput.');

  baseClient.dispose();
}

// ─────────────────────────────────────────────────────────────
// Helpers & custom handler
// ─────────────────────────────────────────────────────────────

void _configureLogging() {
  _logFactory = LoggingBuilder()
      .addConsole()
      .setMinimumLevel(LogLevel.info)
      .build();
}

/// Injects a unique correlation ID into every outgoing request.
///
/// This is a common enterprise pattern for distributed tracing. Each request
/// gets a unique identifier that can be tracked across microservices.
final class _CorrelationIdHandler extends DelegatingHandler {
  _CorrelationIdHandler();

  int _counter = 0;

  @override
  Future<HttpResponse> send(HttpContext context) {
    final id = 'req-${++_counter}-${DateTime.now().millisecondsSinceEpoch}';
    context.updateRequest(
      context.request.withHeader('X-Correlation-Id', id),
    );
    context.setProperty('correlationId', id);
    return innerHandler.send(context);
  }
}
0
likes
160
points
278
downloads

Documentation

API reference

Publisher

verified publisherdavian.space

Weekly Downloads

Production-grade HTTP resilience for Dart & Flutter. Retry, circuit breaker, timeout, bulkhead, hedging, fallback, structured observability, and header-redacted logging.

Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

Dependencies

davianspace_dependencyinjection, davianspace_logging, http

More

Packages that depend on davianspace_http_resilience