Skip to main content
The HTTP Adapter provides a robust interface for calling external REST APIs with built-in resilience patterns including retry logic, circuit breakers, and comprehensive error handling.

Core Interface

IExternalApiClient

The main interface for making HTTP requests to external services.
GetAsync
Task<string>
Performs an HTTP GET request and returns the response as a string.Parameters:
  • url: The endpoint URL
  • authentication: Optional tuple of (scheme, credentials)
  • headers: Optional dictionary of custom headers
Returns: Response body as string
PostAsync<T>
Task<HttpResponseMessage>
Performs an HTTP POST request with a JSON payload.Parameters:
  • uri: The endpoint URL
  • item: The object to serialize and send
  • authentication: Optional tuple of (scheme, credentials)
  • headers: Optional dictionary of custom headers
Returns: The HTTP response message
PutAsync<T>
Task<HttpResponseMessage>
Performs an HTTP PUT request with a JSON payload.Parameters:
  • url: The endpoint URL
  • item: The object to serialize and send
  • authentication: Optional tuple of (scheme, credentials)
  • headers: Optional dictionary of custom headers
Returns: The HTTP response message
DeleteAsync
Task<HttpResponseMessage>
Performs an HTTP DELETE request.Parameters:
  • url: The endpoint URL
  • authentication: Optional tuple of (scheme, credentials)
  • headers: Optional dictionary of custom headers
Returns: The HTTP response message
Core.Application.Adapters.Http/IExternalApiClient.cs
namespace Core.Application
{
    public interface IExternalApiClient
    {
        Task<string> GetAsync(
            string url, 
            Tuple<string, string>? authentication = null, 
            IDictionary<string, string>? headers = null);
        
        Task<HttpResponseMessage> PostAsync<T>(
            string uri, 
            T item, 
            Tuple<string, string>? authentication = null, 
            IDictionary<string, string>? headers = null);
        
        Task<HttpResponseMessage> PutAsync<T>(
            string url, 
            T item, 
            Tuple<string, string>? authentication = null, 
            IDictionary<string, string>? headers = null);
        
        Task<HttpResponseMessage> DeleteAsync(
            string url, 
            Tuple<string, string>? authentication = null, 
            IDictionary<string, string>? headers = null);
    }
}

Implementation

ExternalApiHttpAdapter

The default implementation includes Polly-based resilience patterns.
Core.Infraestructure.Adapters.Http/ExternalApiHttpAdapter.cs
using Core.Application;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.CircuitBreaker;
using Polly.Retry;
using Polly.Wrap;
using System.Net;
using System.Text;

namespace Core.Infraestructure
{
    public class ExternalApiHttpAdapter : IExternalApiClient
    {
        private readonly HttpClient _client;
        private readonly AsyncPolicyWrap<HttpResponseMessage> _policy;
        private readonly ILogger<ExternalApiHttpAdapter> _logger;

        public ExternalApiHttpAdapter(ILogger<ExternalApiHttpAdapter> logger)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _client = new HttpClient();

            // Configure retry policy with exponential backoff
            AsyncRetryPolicy<HttpResponseMessage> retryPolicy = Policy
                .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
                .Or<HttpRequestException>()
                .WaitAndRetryAsync(
                    retryCount: 3,
                    sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
                    onRetry: (outcome, timespan, attempt, context) =>
                    {
                        _logger.LogWarning(
                            $"[Retry] Intento {attempt} después de {timespan.TotalSeconds}s. Resultado: {outcome.Result?.StatusCode}");
                    });

            // Configure circuit breaker
            AsyncCircuitBreakerPolicy<HttpResponseMessage> circuitBreakerPolicy = Policy
                .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
                .Or<HttpRequestException>()
                .CircuitBreakerAsync(
                    handledEventsAllowedBeforeBreaking: 5,
                    durationOfBreak: TimeSpan.FromSeconds(30),
                    onBreak: (outcome, timespan) =>
                    {
                        _logger.LogError(
                            $"[CircuitBreaker] Circuito ABIERTO durante {timespan.TotalSeconds}s");
                    },
                    onReset: () => _logger.LogInformation("[CircuitBreaker] Circuito RESTABLECIDO."),
                    onHalfOpen: () => _logger.LogInformation("[CircuitBreaker] Circuito HALF-OPEN, probando..."));

            // Combine retry and circuit breaker policies
            _policy = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);
        }

        public async Task<string> GetAsync(
            string url, 
            Tuple<string, string>? authentication = null, 
            IDictionary<string, string>? headers = null)
        {
            HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            AddAuthentication(requestMessage, authentication);
            AddHeaders(requestMessage, headers);

            HttpResponseMessage response = await _policy.ExecuteAsync(
                () => _client.SendAsync(requestMessage));

            return await response.Content.ReadAsStringAsync();
        }
    }
}

Resilience Patterns

Retry Policy

The adapter implements exponential backoff retry:
Retry Count
3
Maximum number of retry attempts before giving up.
Backoff Strategy
Exponential
Wait time doubles with each retry: 2s, 4s, 8s.
Trigger Conditions
conditions
  • Non-success HTTP status codes (4xx, 5xx)
  • HttpRequestException thrown

Circuit Breaker

Prevents cascading failures to unhealthy services:
Failure Threshold
5
Circuit opens after 5 consecutive failures.
Break Duration
30 seconds
Time circuit remains open before attempting recovery.
States
states
  • Closed: Normal operation, requests flow through
  • Open: Requests fail fast without calling the service
  • Half-Open: Testing if service has recovered

Retry Logic

Handles transient failures with exponential backoff.

Circuit Breaker

Prevents overwhelming failing services.

Logging

Comprehensive logging of all retry and circuit breaker events.

TLS 1.2

Enforces secure communication protocols.

Registration

Register the HTTP adapter as a singleton:
Infrastructure/Registrations/InfraestructureServicesRegistration.cs
public static IServiceCollection AddInfrastructureServices(
    this IServiceCollection services, 
    IConfiguration configuration)
{
    // Register HTTP adapter
    services.AddSingleton<IExternalApiClient, ExternalApiHttpAdapter>();
    
    return services;
}
Registered as singleton to reuse HttpClient instances and avoid socket exhaustion.

Authentication

The adapter supports two authentication schemes:

Basic Authentication

var credentials = Tuple.Create("basic", "username:password");
var response = await _httpAdapter.GetAsync(
    "https://api.example.com/data",
    authentication: credentials);

Bearer Token Authentication

var token = Tuple.Create("bearer", "your-jwt-token-here");
var response = await _httpAdapter.GetAsync(
    "https://api.example.com/data",
    authentication: token);

Implementation Details

Core.Infraestructure.Adapters.Http/ExternalApiHttpAdapter.cs:136
private void AddAuthentication(HttpRequestMessage requestMessage, Tuple<string, string>? authentication)
{
    if (authentication == null) return;

    if (authentication.Item1.Equals("basic", StringComparison.OrdinalIgnoreCase))
    {
        var base64Auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(authentication.Item2));
        requestMessage.Headers.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", base64Auth);
    }
    else if (authentication.Item1.Equals("bearer", StringComparison.OrdinalIgnoreCase))
    {
        requestMessage.Headers.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authentication.Item2);
    }
}

Custom Headers

Add custom HTTP headers to requests:
var headers = new Dictionary<string, string>
{
    { "X-Custom-Header", "value" },
    { "X-Correlation-Id", Guid.NewGuid().ToString() },
    { "Accept-Language", "en-US" }
};

var response = await _httpAdapter.GetAsync(
    "https://api.example.com/data",
    headers: headers);

Usage Examples

GET Request

Application/ApplicationServices/ExternalDataService.cs
public class ExternalDataService
{
    private readonly IExternalApiClient _httpAdapter;
    private readonly ILogger<ExternalDataService> _logger;

    public ExternalDataService(
        IExternalApiClient httpAdapter,
        ILogger<ExternalDataService> logger)
    {
        _httpAdapter = httpAdapter ?? throw new ArgumentNullException(nameof(httpAdapter));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<ExternalData> GetDataAsync(string id)
    {
        try
        {
            var token = Tuple.Create("bearer", await GetAccessTokenAsync());
            
            string jsonResponse = await _httpAdapter.GetAsync(
                $"https://api.example.com/data/{id}",
                authentication: token);

            return JsonConvert.DeserializeObject<ExternalData>(jsonResponse);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to fetch data for ID: {Id}", id);
            throw;
        }
    }
}

POST Request

public async Task<CreatedResponse> CreateResourceAsync(CreateResourceRequest request)
{
    var token = Tuple.Create("bearer", await GetAccessTokenAsync());
    
    HttpResponseMessage response = await _httpAdapter.PostAsync(
        "https://api.example.com/resources",
        request,
        authentication: token);

    if (response.IsSuccessStatusCode)
    {
        string content = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<CreatedResponse>(content);
    }

    throw new HttpRequestException(
        $"Failed to create resource. Status: {response.StatusCode}");
}

PUT Request

public async Task UpdateResourceAsync(string id, UpdateResourceRequest request)
{
    var token = Tuple.Create("bearer", await GetAccessTokenAsync());
    
    HttpResponseMessage response = await _httpAdapter.PutAsync(
        $"https://api.example.com/resources/{id}",
        request,
        authentication: token);

    if (!response.IsSuccessStatusCode)
    {
        string errorContent = await response.Content.ReadAsStringAsync();
        _logger.LogError(
            "Failed to update resource {Id}. Status: {Status}, Error: {Error}",
            id,
            response.StatusCode,
            errorContent);
        throw new HttpRequestException($"Update failed: {errorContent}");
    }
}

DELETE Request

public async Task DeleteResourceAsync(string id)
{
    var token = Tuple.Create("bearer", await GetAccessTokenAsync());
    
    HttpResponseMessage response = await _httpAdapter.DeleteAsync(
        $"https://api.example.com/resources/{id}",
        authentication: token);

    response.EnsureSuccessStatusCode();
}

Error Handling

The adapter automatically handles common HTTP errors:
Core.Infraestructure.Adapters.Http/ExternalApiHttpAdapter.cs:115
private async Task<HttpResponseMessage> CheckForErrors(HttpResponseMessage response)
{
    string res = await response.Content.ReadAsStringAsync();

    switch (response.StatusCode)
    {
        case HttpStatusCode.InternalServerError:
        case HttpStatusCode.BadRequest:
        case HttpStatusCode.Unauthorized:
            throw new HttpRequestException(res, null, response.StatusCode);

        default:
            return response;
    }
}
try
{
    var result = await _httpAdapter.GetAsync(url, auth);
    return ProcessResult(result);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
    _logger.LogWarning("Authentication failed, refreshing token...");
    await RefreshAuthenticationAsync();
    // Retry with new token
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    _logger.LogWarning("Resource not found: {Url}", url);
    return null;
}
catch (BrokenCircuitException)
{
    _logger.LogError("Circuit breaker is open, service unavailable");
    throw new ServiceUnavailableException("External service is temporarily unavailable");
}

Best Practices

Always use the singleton pattern when registering IExternalApiClient to properly reuse HttpClient instances.
Don’t catch and suppress exceptions from the adapter. Let them propagate or handle them specifically to maintain proper circuit breaker behavior.
Monitor circuit breaker state changes through logs to identify problematic external services.

Advanced Configuration

For custom resilience policies, you can create your own adapter implementation:
public class CustomHttpAdapter : IExternalApiClient
{
    private readonly AsyncPolicyWrap<HttpResponseMessage> _policy;

    public CustomHttpAdapter(ILogger<CustomHttpAdapter> logger)
    {
        // Custom timeout policy
        var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);

        // Custom retry with jitter
        var retryPolicy = Policy
            .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
            .WaitAndRetryAsync(
                5,
                attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)) + 
                          TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)));

        // Custom circuit breaker
        var circuitBreaker = Policy
            .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
            .AdvancedCircuitBreakerAsync(
                failureThreshold: 0.5,
                samplingDuration: TimeSpan.FromSeconds(10),
                minimumThroughput: 20,
                durationOfBreak: TimeSpan.FromSeconds(60));

        _policy = Policy.WrapAsync(timeoutPolicy, retryPolicy, circuitBreaker);
    }
}

Build docs developers (and LLMs) love