Skip to content

HTTP Client in ConnectSoft Microservice Template

Purpose & Overview

HTTP Client in the ConnectSoft Microservice Template provides a robust, production-ready foundation for making HTTP requests to external services and APIs. The template uses IHttpClientFactory to manage HttpClient instances, ensuring proper lifecycle management, connection pooling, and integration with service discovery, header propagation, and distributed tracing.

HTTP Client capabilities include:

  • IHttpClientFactory Integration: Proper HttpClient lifecycle management and connection pooling
  • Service Discovery: Automatic service endpoint resolution via configuration or DNS
  • Header Propagation: Automatic propagation of correlation and tracing headers
  • OpenTelemetry Instrumentation: Automatic distributed tracing for outgoing HTTP requests
  • Resilience Patterns: Support for retry policies, circuit breakers, and timeout handling
  • Type Safety: Typed client support for strongly-typed service clients

HTTP Client Philosophy

The ConnectSoft Microservice Template uses IHttpClientFactory exclusively for creating HttpClient instances. This ensures proper resource management, connection pooling, and integration with service discovery and observability features. Direct instantiation of HttpClient is avoided to prevent socket exhaustion and connection leaks.

Architecture Overview

HTTP Client Factory Pattern

Service Layer
├── Service Uses IHttpClientFactory
│   ├── CreateClient() → HttpClient Instance
│   ├── CreateClient("NamedClient") → Named HttpClient
│   └── Typed Client (Injected via DI)
└── HTTP Request
    ├── Header Propagation
    ├── Service Discovery Resolution
    ├── OpenTelemetry Instrumentation
    └── Resilience Policies (if configured)
External Service

HttpClient Lifecycle

Problem with Direct Instantiation:

// ❌ BAD - Socket exhaustion risk
public class BadService
{
    public async Task CallApi()
    {
        using (var client = new HttpClient()) // Disposes socket but doesn't release immediately
        {
            // Socket remains in TIME_WAIT state
        }
    }
}

Solution with IHttpClientFactory:

// ✅ GOOD - Proper connection pooling
public class GoodService
{
    private readonly IHttpClientFactory httpClientFactory;

    public GoodService(IHttpClientFactory httpClientFactory)
    {
        this.httpClientFactory = httpClientFactory;
    }

    public async Task CallApi()
    {
        var client = this.httpClientFactory.CreateClient(); // Reuses pooled connections
        // HttpClient instance is managed by factory
        // No need to dispose - factory handles lifecycle
    }
}

Configuration

Basic Setup

Service Registration:

// HttpClientExtensions.cs
internal static IServiceCollection AddMicroserviceHttpClient(
    this IServiceCollection services)
{
    ArgumentNullException.ThrowIfNull(services);

    // Configure HttpClient defaults
    services.ConfigureHttpClientDefaults(http =>
    {
        if (OptionsExtensions.ServiceDiscoveryOptions.Enabled)
        {
            http.AddServiceDiscovery(); // Enable service discovery for all clients
        }
    });

    // Add HttpClient factory with header propagation
    services.AddHttpClient()
        .AddHeaderPropagation(); // Automatically propagate headers

    return services;
}

Integration in Application:

// MicroserviceRegistrationExtensions.cs
services.AddMicroserviceHeaderPropagation(); // Register header propagation services
services.AddMicroserviceHttpClient(); // Register HttpClient factory

Service Discovery Integration

Service Discovery Configuration:

{
  "ServiceDiscovery": {
    "Enabled": true,
    "ServiceDiscoveryProvider": "Configuration",
    "ConfigurationServiceEndpointSectionName": "Services"
  },
  "Services": {
    "payment-service": {
      "http": ["localhost:5001"],
      "https": ["localhost:5002"]
    },
    "order-service": {
      "http": ["localhost:5003"],
      "https": ["localhost:5004"]
    }
  }
}

Usage with Service Discovery:

// Point to logical service name
services.AddHttpClient<PaymentServiceClient>(client =>
{
    client.BaseAddress = new Uri("https://payment-service"); // Logical name
    // Service discovery resolves to actual endpoint at runtime
});

Service Discovery Providers:

Provider Configuration Use Case
Configuration appsettings.json Development, testing, static endpoints
DNS DNS A/AAAA records Kubernetes, DNS-based service discovery
DNS SRV DNS SRV records Advanced DNS-based discovery

Header Propagation

Automatic Header Propagation:

Headers are automatically propagated from incoming requests to outgoing HTTP requests:

  • X-TraceId: Custom trace identifier
  • X-Correlation-Id: Business correlation identifier
  • traceparent: W3C Trace Context header (OpenTelemetry)

See Header Propagation for detailed documentation.

Configuration:

// HeaderPropagationExtensions.cs
services.AddHeaderPropagation(options =>
{
    options.Headers.Add("X-TraceId");
    options.Headers.Add("X-Correlation-Id");
    options.Headers.Add("traceparent");
});

// Add to HttpClient factory
services.AddHttpClient()
    .AddHeaderPropagation();

OpenTelemetry Instrumentation

Automatic Instrumentation:

// OpenTelemetryExtensions.cs
services.AddOpenTelemetry()
    .WithTracing(builder => builder
        .AddHttpClientInstrumentation(options =>
        {
            // Filter out telemetry endpoints
            options.FilterHttpRequestMessage = message =>
                message is not null &&
                message.RequestUri is not null &&
                !message.RequestUri.Host.Contains("visualstudio", StringComparison.Ordinal) &&
                !message.RequestUri.Host.Contains("applicationinsights", StringComparison.Ordinal);
        }));

Features: - Automatic span creation for outgoing HTTP requests - Trace context propagation via traceparent header - Request/response metadata capture - Error tracking and status code recording

See Distributed Tracing for detailed documentation.

Usage Patterns

1. Standard HttpClient

Create Client:

public class OrderService
{
    private readonly IHttpClientFactory httpClientFactory;
    private readonly ILogger<OrderService> logger;

    public OrderService(
        IHttpClientFactory httpClientFactory,
        ILogger<OrderService> logger)
    {
        this.httpClientFactory = httpClientFactory;
        this.logger = logger;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(Order order)
    {
        // Create HttpClient instance (managed by factory)
        var client = this.httpClientFactory.CreateClient();

        try
        {
            // Headers are automatically propagated
            var response = await client.PostAsJsonAsync(
                "https://payment-service/api/payments",
                new PaymentRequest { OrderId = order.Id });

            response.EnsureSuccessStatusCode();

            return await response.Content.ReadFromJsonAsync<PaymentResult>();
        }
        catch (HttpRequestException ex)
        {
            this.logger.LogError(
                ex,
                "Failed to process payment for order {OrderId}",
                order.Id);
            throw;
        }
    }
}

Characteristics: - ✅ No need to dispose HttpClient (factory manages lifecycle) - ✅ Connection pooling and reuse - ✅ Header propagation enabled automatically - ✅ OpenTelemetry instrumentation enabled

2. Named HttpClient

Register Named Client:

// In service registration
services.AddHttpClient("PaymentService", client =>
{
    client.BaseAddress = new Uri("https://payment-service");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("X-Api-Version", "v1");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHeaderPropagation(); // Ensure header propagation

Use Named Client:

public class OrderService
{
    private readonly IHttpClientFactory httpClientFactory;

    public OrderService(IHttpClientFactory httpClientFactory)
    {
        this.httpClientFactory = httpClientFactory;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(Order order)
    {
        // Get named client (pre-configured)
        var client = this.httpClientFactory.CreateClient("PaymentService");

        // Use relative URL (BaseAddress is already set)
        var response = await client.PostAsJsonAsync(
            "/api/payments",
            new PaymentRequest { OrderId = order.Id });

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<PaymentResult>();
    }
}

Advantages: - ✅ Pre-configured base address and headers - ✅ Consistent configuration across usage - ✅ Easy to update configuration in one place

Define Typed Client:

public interface IPaymentServiceClient
{
    Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request);
    Task<PaymentStatus> GetPaymentStatusAsync(string paymentId);
}

public class PaymentServiceClient : IPaymentServiceClient
{
    private readonly HttpClient httpClient;
    private readonly ILogger<PaymentServiceClient> logger;

    public PaymentServiceClient(
        HttpClient httpClient,
        ILogger<PaymentServiceClient> logger)
    {
        this.httpClient = httpClient;
        this.logger = logger;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        try
        {
            var response = await this.httpClient.PostAsJsonAsync(
                "/api/payments",
                request);

            response.EnsureSuccessStatusCode();

            return await response.Content.ReadFromJsonAsync<PaymentResult>();
        }
        catch (HttpRequestException ex)
        {
            this.logger.LogError(
                ex,
                "Failed to process payment: {PaymentRequest}",
                request);
            throw;
        }
    }

    public async Task<PaymentStatus> GetPaymentStatusAsync(string paymentId)
    {
        var response = await this.httpClient.GetAsync($"/api/payments/{paymentId}");
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<PaymentStatus>();
    }
}

Register Typed Client:

// In service registration
services.AddHttpClient<IPaymentServiceClient, PaymentServiceClient>(client =>
{
    client.BaseAddress = new Uri("https://payment-service");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("X-Api-Version", "v1");
})
.AddHeaderPropagation(); // Ensure header propagation

Use Typed Client:

public class OrderService
{
    private readonly IPaymentServiceClient paymentServiceClient;

    public OrderService(IPaymentServiceClient paymentServiceClient)
    {
        this.paymentServiceClient = paymentServiceClient;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(Order order)
    {
        // Type-safe client usage
        return await this.paymentServiceClient.ProcessPaymentAsync(
            new PaymentRequest { OrderId = order.Id });
    }
}

Advantages: - ✅ Type-safe service contracts - ✅ Encapsulated HTTP logic - ✅ Easy to test (mockable interface) - ✅ Single responsibility principle - ✅ Dependency injection ready

4. Configuration-Based Typed Clients

Configuration:

{
  "HttpClients": {
    "PaymentService": {
      "BaseAddress": "https://payment-service",
      "Timeout": "00:00:30",
      "DefaultHeaders": {
        "X-Api-Version": "v1"
      }
    }
  }
}

Register from Configuration:

services.AddHttpClient<IPaymentServiceClient, PaymentServiceClient>((serviceProvider, client) =>
{
    var configuration = serviceProvider.GetRequiredService<IConfiguration>();
    var section = configuration.GetSection("HttpClients:PaymentService");

    client.BaseAddress = new Uri(section["BaseAddress"]);
    client.Timeout = TimeSpan.Parse(section["Timeout"]);

    var headers = section.GetSection("DefaultHeaders");
    foreach (var header in headers.GetChildren())
    {
        client.DefaultRequestHeaders.Add(header.Key, header.Value);
    }
})
.AddHeaderPropagation();

Advanced Configuration

Timeout Configuration

Client-Level Timeout:

services.AddHttpClient<PaymentServiceClient>(client =>
{
    client.Timeout = TimeSpan.FromSeconds(30);
});

Request-Level Timeout:

public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

    var response = await this.httpClient.PostAsJsonAsync(
        "/api/payments",
        request,
        cts.Token);

    return await response.Content.ReadFromJsonAsync<PaymentResult>();
}

Custom Headers

Per-Request Headers:

public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
    var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/payments")
    {
        Content = JsonContent.Create(request)
    };

    // Add custom headers
    httpRequest.Headers.Add("X-Request-Id", Guid.NewGuid().ToString());
    httpRequest.Headers.Add("X-User-Id", userId);

    var response = await this.httpClient.SendAsync(httpRequest);
    return await response.Content.ReadFromJsonAsync<PaymentResult>();
}

Default Headers (applied to all requests):

services.AddHttpClient<PaymentServiceClient>(client =>
{
    client.DefaultRequestHeaders.Add("X-Api-Version", "v1");
    client.DefaultRequestHeaders.Add("Accept", "application/json");

    // Authorization header (if static)
    client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Bearer", "static-token");
});

Authentication

Bearer Token Authentication:

public class PaymentServiceClient
{
    private readonly HttpClient httpClient;
    private readonly ITokenProvider tokenProvider;

    public PaymentServiceClient(
        HttpClient httpClient,
        ITokenProvider tokenProvider)
    {
        this.httpClient = httpClient;
        this.tokenProvider = tokenProvider;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        // Get token dynamically
        var token = await this.tokenProvider.GetTokenAsync();

        var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/payments")
        {
            Content = JsonContent.Create(request)
        };

        httpRequest.Headers.Authorization = 
            new AuthenticationHeaderValue("Bearer", token);

        var response = await this.httpClient.SendAsync(httpRequest);
        return await response.Content.ReadFromJsonAsync<PaymentResult>();
    }
}

Using DelegatingHandler:

public class AuthenticationHandler : DelegatingHandler
{
    private readonly ITokenProvider tokenProvider;

    public AuthenticationHandler(ITokenProvider tokenProvider)
    {
        this.tokenProvider = tokenProvider;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = await this.tokenProvider.GetTokenAsync();
        request.Headers.Authorization = 
            new AuthenticationHeaderValue("Bearer", token);

        return await base.SendAsync(request, cancellationToken);
    }
}

// Register handler
services.AddHttpClient<PaymentServiceClient>()
    .AddHttpMessageHandler<AuthenticationHandler>()
    .AddHeaderPropagation();

Resilience Patterns

Retry Policy (using Polly):

services.AddHttpClient<PaymentServiceClient>()
    .AddPolicyHandler(GetRetryPolicy())
    .AddHeaderPropagation();

private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return Policy<HttpResponseMessage>
        .Handle<HttpRequestException>()
        .OrResult(r => (int)r.StatusCode >= 500)
        .WaitAndRetryAsync(
            retryCount: 3,
            sleepDurationProvider: retryAttempt => 
                TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
            onRetry: (outcome, timespan, retryCount, context) =>
            {
                var logger = context.GetLogger();
                logger?.LogWarning(
                    "Retry {RetryCount} after {Delay}s",
                    retryCount,
                    timespan.TotalSeconds);
            });
}

Circuit Breaker Policy:

services.AddHttpClient<PaymentServiceClient>()
    .AddPolicyHandler(GetCircuitBreakerPolicy())
    .AddHeaderPropagation();

private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return Policy<HttpResponseMessage>
        .Handle<HttpRequestException>()
        .OrResult(r => (int)r.StatusCode >= 500)
        .CircuitBreakerAsync(
            handledEventsAllowedBeforeBreaking: 5,
            durationOfBreak: TimeSpan.FromSeconds(30),
            onBreak: (result, duration) =>
            {
                // Log circuit breaker opened
            },
            onReset: () =>
            {
                // Log circuit breaker reset
            });
}

Combined Policies:

services.AddHttpClient<PaymentServiceClient>()
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy())
    .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)))
    .AddHeaderPropagation();

See Resiliency for detailed resilience pattern documentation.

Error Handling

Standard Error Handling

public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
    try
    {
        var response = await this.httpClient.PostAsJsonAsync(
            "/api/payments",
            request);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<PaymentResult>();
    }
    catch (HttpRequestException ex)
    {
        this.logger.LogError(
            ex,
            "HTTP error calling payment service: {Request}",
            request);
        throw;
    }
    catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
    {
        this.logger.LogError(
            ex,
            "Timeout calling payment service: {Request}",
            request);
        throw new PaymentServiceTimeoutException("Payment service timeout", ex);
    }
    catch (JsonException ex)
    {
        this.logger.LogError(
            ex,
            "Failed to deserialize payment response");
        throw;
    }
}

Handling Specific Status Codes

public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
    var response = await this.httpClient.PostAsJsonAsync(
        "/api/payments",
        request);

    switch (response.StatusCode)
    {
        case HttpStatusCode.OK:
            return await response.Content.ReadFromJsonAsync<PaymentResult>();

        case HttpStatusCode.BadRequest:
            var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
            throw new ValidationException(error.Message);

        case HttpStatusCode.NotFound:
            throw new PaymentServiceNotFoundException("Payment service not found");

        case HttpStatusCode.ServiceUnavailable:
            throw new PaymentServiceUnavailableException("Payment service unavailable");

        default:
            response.EnsureSuccessStatusCode();
            return null;
    }
}

ProblemDetails Support

public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
    var response = await this.httpClient.PostAsJsonAsync(
        "/api/payments",
        request);

    if (!response.IsSuccessStatusCode)
    {
        // Try to read ProblemDetails
        if (response.Content.Headers.ContentType?.MediaType == "application/problem+json")
        {
            var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
            throw new PaymentServiceException(
                problem.Title ?? "Payment service error",
                problem);
        }

        response.EnsureSuccessStatusCode();
    }

    return await response.Content.ReadFromJsonAsync<PaymentResult>();
}

Testing

Unit Testing with Mock HttpClient

Using Moq:

[TestMethod]
public async Task ProcessPayment_ShouldReturnResult()
{
    // Arrange
    var mockHandler = new Mock<HttpMessageHandler>();
    var response = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = JsonContent.Create(new PaymentResult { Id = "123" })
    };

    mockHandler
        .Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(response);

    var httpClient = new HttpClient(mockHandler.Object)
    {
        BaseAddress = new Uri("https://payment-service")
    };

    var client = new PaymentServiceClient(
        httpClient,
        Mock.Of<ILogger<PaymentServiceClient>>());

    // Act
    var result = await client.ProcessPaymentAsync(
        new PaymentRequest { OrderId = "order-123" });

    // Assert
    Assert.IsNotNull(result);
    Assert.AreEqual("123", result.Id);
}

Integration Testing with TestServer

public class PaymentServiceClientTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> factory;
    private readonly HttpClient httpClient;

    public PaymentServiceClientTests(WebApplicationFactory<Program> factory)
    {
        this.factory = factory;
        this.httpClient = this.factory.CreateClient();
    }

    [TestMethod]
    public async Task ProcessPayment_ShouldReturnResult()
    {
        // Arrange
        var request = new PaymentRequest { OrderId = "order-123" };

        // Act
        var response = await this.httpClient.PostAsJsonAsync(
            "/api/payments",
            request);

        // Assert
        response.EnsureSuccessStatusCode();
        var result = await response.Content.ReadFromJsonAsync<PaymentResult>();
        Assert.IsNotNull(result);
    }
}

Testing with HttpMock

Using RichardSzalay.MockHttp:

[TestMethod]
public async Task ProcessPayment_ShouldReturnResult()
{
    // Arrange
    var mockHttp = new MockHttpMessageHandler();
    mockHttp
        .When("https://payment-service/api/payments")
        .Respond("application/json", JsonSerializer.Serialize(new PaymentResult { Id = "123" }));

    var httpClient = mockHttp.ToHttpClient();
    httpClient.BaseAddress = new Uri("https://payment-service");

    var client = new PaymentServiceClient(
        httpClient,
        Mock.Of<ILogger<PaymentServiceClient>>());

    // Act
    var result = await client.ProcessPaymentAsync(
        new PaymentRequest { OrderId = "order-123" });

    // Assert
    Assert.IsNotNull(result);
    Assert.AreEqual("123", result.Id);
}

Best Practices

Do's

  1. Always Use IHttpClientFactory

    // ✅ GOOD
    var client = httpClientFactory.CreateClient();
    
    // ❌ BAD - Socket exhaustion risk
    using (var client = new HttpClient()) { }
    

  2. Use Typed Clients for Service Clients

    // ✅ GOOD - Type-safe, testable, encapsulated
    services.AddHttpClient<IPaymentServiceClient, PaymentServiceClient>();
    
    // ❌ BAD - Magic strings, scattered configuration
    var client = httpClientFactory.CreateClient("PaymentService");
    

  3. Enable Header Propagation

    // ✅ GOOD - Automatic correlation
    services.AddHttpClient<PaymentServiceClient>()
        .AddHeaderPropagation();
    

  4. Configure Timeouts

    // ✅ GOOD - Prevents hanging requests
    services.AddHttpClient<PaymentServiceClient>(client =>
    {
        client.Timeout = TimeSpan.FromSeconds(30);
    });
    

  5. Handle Errors Gracefully

    // ✅ GOOD - Proper error handling
    try
    {
        var response = await client.PostAsJsonAsync(...);
        response.EnsureSuccessStatusCode();
    }
    catch (HttpRequestException ex)
    {
        logger.LogError(ex, "HTTP request failed");
        throw;
    }
    

  6. Use Service Discovery for Logical Names

    // ✅ GOOD - Service discovery resolves endpoint
    client.BaseAddress = new Uri("https://payment-service");
    
    // ❌ BAD - Hardcoded endpoint
    client.BaseAddress = new Uri("https://payment-service.internal:5001");
    

  7. Log HTTP Requests

    // ✅ GOOD - Observability
    logger.LogInformation(
        "Calling payment service: {Method} {Uri}",
        request.Method,
        request.RequestUri);
    

Don'ts

  1. Don't Dispose HttpClient from Factory

    // ❌ BAD - Factory manages lifecycle
    using (var client = httpClientFactory.CreateClient()) { }
    
    // ✅ GOOD - Factory manages lifecycle
    var client = httpClientFactory.CreateClient();
    

  2. Don't Create HttpClient Directly

    // ❌ BAD - Socket exhaustion
    var client = new HttpClient();
    
    // ✅ GOOD - Use factory
    var client = httpClientFactory.CreateClient();
    

  3. Don't Modify BaseAddress After Creation

    // ❌ BAD - Thread safety issues
    var client = httpClientFactory.CreateClient("PaymentService");
    client.BaseAddress = new Uri("https://other-service");
    
    // ✅ GOOD - Configure during registration
    services.AddHttpClient("PaymentService", client =>
    {
        client.BaseAddress = new Uri("https://payment-service");
    });
    

  4. Don't Store HttpClient in Fields

    // ❌ BAD - HttpClient is not thread-safe for all operations
    public class BadService
    {
        private readonly HttpClient client;
    
        public BadService(IHttpClientFactory factory)
        {
            this.client = factory.CreateClient(); // Don't store
        }
    }
    
    // ✅ GOOD - Create per request or use typed client
    public class GoodService
    {
        private readonly IHttpClientFactory factory;
    
        public async Task CallApi()
        {
            var client = this.factory.CreateClient(); // Create per request
        }
    }
    

  5. Don't Ignore Cancellation Tokens

    // ❌ BAD - Can't cancel
    var response = await client.PostAsJsonAsync("/api/payments", request);
    
    // ✅ GOOD - Respects cancellation
    var response = await client.PostAsJsonAsync(
        "/api/payments",
        request,
        cancellationToken);
    

Troubleshooting

Issue: Socket Exhaustion

Symptoms: SocketException with "Too many open files" or connection timeout errors.

Solution: - ✅ Use IHttpClientFactory instead of direct HttpClient instantiation - ✅ Don't dispose HttpClient instances from factory - ✅ Use typed clients for service clients

Issue: Headers Not Propagated

Symptoms: Missing correlation/trace headers in outgoing requests.

Solution: - ✅ Verify AddHeaderPropagation() is called on HttpClient registration - ✅ Ensure UseMicroserviceHeaderPropagation() middleware is registered - ✅ Check middleware order (header propagation must be after routing)

Issue: Service Discovery Not Working

Symptoms: HttpRequestException with "No such host" or DNS resolution errors.

Solution: - ✅ Verify service discovery is enabled in configuration - ✅ Check service endpoint configuration in appsettings.json - ✅ Ensure service name matches configuration key - ✅ Verify DNS provider is configured correctly (if using DNS provider)

Issue: Timeout Errors

Symptoms: TaskCanceledException with timeout errors.

Solution: - ✅ Configure appropriate timeout on HttpClient - ✅ Use request-level timeout for long-running operations - ✅ Consider increasing timeout for specific operations - ✅ Check network connectivity and service availability

Issue: Connection Pool Exhaustion

Symptoms: Requests hanging or slow response times.

Solution: - ✅ Verify IHttpClientFactory is used - ✅ Check for connection leaks (disposing HttpClient incorrectly) - ✅ Monitor connection pool metrics - ✅ Consider increasing ServicePointManager.DefaultConnectionLimit

Summary

HTTP Client in the ConnectSoft Microservice Template provides:

  • IHttpClientFactory Integration: Proper lifecycle management and connection pooling
  • Service Discovery: Automatic endpoint resolution via configuration or DNS
  • Header Propagation: Automatic correlation and tracing header propagation
  • OpenTelemetry Instrumentation: Automatic distributed tracing
  • Type Safety: Typed client support for strongly-typed service clients
  • Resilience Patterns: Support for retry, circuit breaker, and timeout policies
  • Best Practices: Socket exhaustion prevention and proper resource management

By following these patterns, teams can:

  • Build Reliable Services: Proper HTTP client lifecycle management prevents socket exhaustion
  • Enable Observability: Automatic tracing and correlation for distributed systems
  • Simplify Configuration: Service discovery eliminates hardcoded endpoints
  • Ensure Consistency: Typed clients provide type-safe, testable service clients
  • Handle Failures Gracefully: Resilience patterns and error handling for production readiness

The HTTP client infrastructure ensures that ConnectSoft microservices can reliably communicate with external services while maintaining observability, consistency, and proper resource management.