davianspace_http_resilience 1.0.4 copy "davianspace_http_resilience: ^1.0.4" to clipboard
davianspace_http_resilience: ^1.0.4 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)
///  11. Map body auto-encoding (JSON)
///  12. Transport-agnostic policy engine
///  13. Observability & event hub
///  14. Error handling patterns
///  15. Fallback with event hub integration
///
/// 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();
  await _example11MapBodyAutoEncoding();
  _example12TransportAgnosticPolicies();
  _example13ObservabilityEventHub();
  await _example14ErrorHandling();
  _example15FallbackWithEventHub();
  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();
}

// ─────────────────────────────────────────────────────────────
// 11. Map body auto-encoding (JSON)
// ─────────────────────────────────────────────────────────────

/// Demonstrates the automatic JSON encoding of `Map<String, dynamic>` bodies.
///
/// In v1.0.4, `ResilientHttpClient` verb helpers (`post`, `put`, `patch`)
/// detect `Map<String, dynamic>` and `Map<String, Object?>` bodies and
/// automatically call `jsonEncode()` + set `Content-Type: application/json`.
Future<void> _example11MapBodyAutoEncoding() async {
  print('\n[Example 11] Map body auto-encoding (JSON)');

  final client = HttpClientBuilder('auto-json')
      .withBaseUri(Uri.parse('https://jsonplaceholder.typicode.com'))
      .withLogging(logger: _logFactory.createLogger('davianspace.http'))
      .withRetry(RetryPolicy.constant(maxRetries: 1))
      .build();

  try {
    // Pass a Map directly — no jsonEncode() or Content-Type header needed!
    final response = await client.post(
      Uri.parse('/posts'),
      body: {
        'title': 'Auto-encoded post',
        'body': 'This map was automatically JSON-encoded',
        'userId': 1,
      },
    );

    final json = response.ensureSuccess().bodyAsJsonMap;
    print('  → Created post id: ${json?['id']}');
    print('  → Status: ${response.statusCode}');

    // String bodies still work as before
    final stringResponse = await client.post(
      Uri.parse('/posts'),
      body: jsonEncode({'title': 'Manual encode', 'userId': 1}),
      headers: {'Content-Type': 'application/json'},
    );
    print('  → String body status: ${stringResponse.statusCode}');
  } on HttpStatusException catch (e) {
    print('  ✗ HTTP error: $e');
  } finally {
    client.dispose();
  }
}

// ─────────────────────────────────────────────────────────────
// 12. Transport-agnostic policy engine
// ─────────────────────────────────────────────────────────────

/// Demonstrates using resilience policies independent of HTTP.
///
/// `Policy.wrap` and `ResiliencePipelineBuilder` work with any async
/// operation — database calls, gRPC, file I/O, etc.
void _example12TransportAgnosticPolicies() {
  print('\n[Example 12] Transport-agnostic policy engine');

  // Build a composite policy from the Policy factory
  final policy = Policy.wrap([
    Policy.retry(maxRetries: 3),
    Policy.timeout(const Duration(seconds: 5)),
    Policy.bulkheadIsolation(maxConcurrentRequests: 15),
  ]);

  final policyWrap = policy as PolicyWrap;
  print('  → Policy created: ${policy.runtimeType}');
  print('  → Contains ${policyWrap.policies.length} nested policies');

  for (final p in policyWrap.policies) {
    print('    • ${p.runtimeType}');
  }

  // Also available via the fluent builder
  final pipeline = ResiliencePipelineBuilder()
      .addPolicy(RetryResiliencePolicy(
        maxRetries: 2,
        backoff: const ConstantBackoff(Duration(milliseconds: 100)),
        onRetry: (attempt, exception) {
          print('  → Retry attempt $attempt: $exception');
        },
      ),)
      .addPolicy(
        const TimeoutResiliencePolicy(
           Duration(seconds: 3),
        ),
      )
      .build();

  print('  → Fluent pipeline: ${pipeline.runtimeType}');

  // Usage example (not executed — just demonstrates the API):
  //   final result = await policy.execute(() => myDatabase.query('...'));
  //   final rows  = await pipeline.execute(() => grpcClient.fetchAll());

  policy.dispose();
  pipeline.dispose();
}

// ─────────────────────────────────────────────────────────────
// 13. Observability & event hub
// ─────────────────────────────────────────────────────────────

/// Demonstrates subscribing to resilience lifecycle events via the event hub.
///
/// The `ResilienceEventHub` provides both type-safe and catch-all subscriptions
/// for retry, circuit breaker, timeout, fallback, and bulkhead events.
void _example13ObservabilityEventHub() {
  print('\n[Example 13] Observability & event hub');

  final hub = ResilienceEventHub();

  // Type-safe subscription for specific event types
  hub.on<RetryEvent>((event) {
    print('  → [RetryEvent] attempt=${event.attemptNumber}');
  });

  hub.on<CircuitOpenEvent>((event) {
    print('  → [CircuitOpenEvent] circuit=${event.circuitName}');
  });

  hub.on<TimeoutEvent>((event) {
    print('  → [TimeoutEvent] $event');
  });

  hub.on<FallbackEvent>((event) {
    print('  → [FallbackEvent] $event');
  });

  hub.on<BulkheadRejectedEvent>((event) {
    print('  → [BulkheadRejectedEvent] $event');
  });

  // Catch-all subscription for logging / diagnostics
  hub.onAny((event) {
    print('  → [ANY] ${event.runtimeType}');
  });

  // Emit synthetic events to show the subscriptions in action
  hub.emit(RetryEvent(
    attemptNumber: 1,
    delay: const Duration(milliseconds: 200),
    maxAttempts: 3,
  ),);
  hub.emit(CircuitOpenEvent(
    circuitName: 'demo-circuit',
    previousState: CircuitState.closed,
    consecutiveFailures: 5,
  ),);
  hub.emit(FallbackEvent());

  // Introspection
  print('  → Active listeners: ${hub.listenerCount}');
  print('  → Has listeners: ${hub.isNotEmpty}');

  hub.clear();
  print('  → Cleared all listeners');
}

// ─────────────────────────────────────────────────────────────
// 14. Error handling patterns
// ─────────────────────────────────────────────────────────────

/// Demonstrates the structured exception hierarchy.
///
/// All resilience exceptions extend `HttpResilienceException`, enabling
/// both fine-grained and catch-all error handling.
Future<void> _example14ErrorHandling() async {
  print('\n[Example 14] Error handling patterns');

  final client = HttpClientBuilder('error-demo')
      .withBaseUri(Uri.parse('https://jsonplaceholder.typicode.com'))
      .withRetry(RetryPolicy.constant(
        maxRetries: 1,
      ),)
      .withCircuitBreaker(const CircuitBreakerPolicy(
        circuitName: 'error-demo',
        failureThreshold: 10, // high threshold so we don't trip
      ),)
      .withTimeout(const TimeoutPolicy(timeout: Duration(seconds: 10)))
      .build();

  // Demonstrate HttpStatusException from ensureSuccess()
  try {
    final response = await client.get(Uri.parse('/posts/99999'));
    response.ensureSuccess(); // throws if non-2xx
    print('  → Success: ${response.bodyAsString}');
  } on HttpStatusException catch (e) {
    print('  → Caught HttpStatusException: status=${e.statusCode}');
  }

  // Demonstrate CancellationException
  try {
    final token = CancellationToken();
    token.cancel('demo cancellation');
    token.throwIfCancelled();
  } on CancellationException catch (e) {
    print('  → Caught CancellationException: reason=${e.reason}');
  }

  // Demonstrate exception hierarchy via try/catch:
  // HttpResilienceException is itself an Exception
  try {
    throw const HttpResilienceException('hierarchy demo');
  } on Exception catch (e) {
    print('  → HttpResilienceException caught as Exception (${e.runtimeType})');
  }

  // HttpTimeoutException extends HttpResilienceException
  try {
    throw HttpTimeoutException(timeout: const Duration(seconds: 5));
  } on HttpResilienceException catch (e) {
    print('  → HttpTimeoutException caught as HttpResilienceException '
        '(${e.runtimeType})');
  }

  client.dispose();
}

// ─────────────────────────────────────────────────────────────
// 15. Fallback with event hub integration
// ─────────────────────────────────────────────────────────────

/// Demonstrates FallbackPolicy with `eventHub` for error reporting.
///
/// In v1.0.4, `FallbackPolicy` can accept an optional `ResilienceEventHub`.
/// If the user's fallback callback throws, the error is caught and emitted
/// as a `FallbackCallbackErrorEvent` instead of being silently swallowed.
void _example15FallbackWithEventHub() {
  print('\n[Example 15] Fallback with event hub integration');

  final hub = ResilienceEventHub();

  // Listen for fallback callback errors
  hub.on<FallbackCallbackErrorEvent>((event) {
    print('  → [FallbackCallbackErrorEvent]');
    print('    original error : ${event.originalError}');
    print('    callback error : ${event.callbackError}');
  });

  // Build a fallback policy that reports errors to the event hub
  final fallback = FallbackPolicy(
    eventHub: hub,
    fallbackAction: (context, error, stackTrace) async => HttpResponse(
      statusCode: 200,
      body: utf8.encode('{"cached": true, "source": "fallback"}'),
    ),
    shouldHandle: (response, exception, context) {
      if (exception != null) return true;
      return response != null &&
          [500, 502, 503, 504].contains(response.statusCode);
    },
  );

  print('  → FallbackPolicy created with eventHub');
  print('  → shouldHandle 503: ${fallback.shouldHandle!(HttpResponse(statusCode: 503), null, HttpContext(request: HttpRequest(method: HttpMethod.get, uri: Uri.parse("https://example.com"))))}');
  print('  → shouldHandle 200: ${fallback.shouldHandle!(HttpResponse(statusCode: 200), null, HttpContext(request: HttpRequest(method: HttpMethod.get, uri: Uri.parse("https://example.com"))))}');

  hub.clear();
}

// ─────────────────────────────────────────────────────────────
// 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