.NET by Patrik

Building Reliable HTTP Client Services in .NET: A Practical Guide

Creating reliable HTTP client services is a challenge for many .NET developers. Network timeouts, throttling, retries, and unexpected exceptions often lead to inconsistent logging, unclear error messages, and unstable public APIs. This Snipp gives an overview of how to design a clean, predictable, and well-structured error-handling strategy for your HTTP-based services.

Readers will learn why custom exceptions matter, how to log different failure types correctly, and how to build a stable exception boundary that hides internal details from users of a library. Each child Snipp focuses on one topic and includes practical examples. Together, they offer a clear blueprint for building services that are easier to debug, test, and maintain.

The overall goal is simple: Create a .NET service that logs clearly, behaves consistently, and protects callers from internal complexity.

dotnet
httpclient
architecture
errors
reliability
...see more

Issue

Services that rely on HttpClient may expose raw exceptions such as HttpRequestException or TaskCanceledException. This forces callers to understand internal behavior and makes error handling unpredictable.

Cause

Every network error produces a different exception. Adding retry handlers or message handlers can introduce even more. When these are not wrapped or unified, the public API leaks internal implementation details.

Resolution

Wrap internal exceptions in a domain-specific exception (for example, ServiceClientException). Keep the original exception as the InnerException. This creates a predictable and stable API surface.

try
{
    var response = await _httpClient.SendAsync(request);
    response.EnsureSuccessStatusCode();
}
catch (Exception ex) when (ex is HttpRequestException ||
                           ex is TaskCanceledException ||
                           ex is OperationCanceledException)
{
    throw new ServiceClientException("Failed to execute remote request.", ex);
}

Custom exceptions protect your API and allow internal changes without breaking callers.

...see more

Issue

HTTP calls fail for many reasons: timeouts, throttling, network issues, or retry exhaustion. Logging only one exception type results in missing or inconsistent diagnostic information.

Cause

Most implementations log only HttpRequestException, ignoring other relevant exceptions like retry errors or cancellation events. Over time, this makes troubleshooting difficult and logs incomplete.

Resolution

Use a single unified logging method that handles all relevant exception types. Apply specific messages for each category while keeping the logic in one place.

private void LogServiceException(Exception ex)
{
    switch (ex)
    {
        case HttpRequestException httpEx:
            LogHttpRequestException(httpEx);
            break;

        case RetryException retryEx:
            _logger.LogError("Retry exhausted. Last status: {Status}. Exception: {Ex}",
                retryEx.StatusCode, retryEx);
            break;

        case TaskCanceledException:
            _logger.LogError("Request timed out. Exception: {Ex}", ex);
            break;

        case OperationCanceledException:
            _logger.LogError("Operation was cancelled. Exception: {Ex}", ex);
            break;

        default:
            _logger.LogError("Unexpected error occurred. Exception: {Ex}", ex);
            break;
    }
}

private void LogHttpRequestException(HttpRequestException ex)
{
    if (ex.StatusCode == HttpStatusCode.NotFound)
        _logger.LogError("Resource not found. Exception: {Ex}", ex);
    else if (ex.StatusCode == HttpStatusCode.TooManyRequests)
        _logger.LogError("Request throttled. Exception: {Ex}", ex);
    else
        _logger.LogError("HTTP request failed ({Status}). Exception: {Ex}",
            ex.StatusCode, ex);
}

Centralizing logic ensures consistent, clear, and maintainable logging across all error paths.

...see more

Issue

Libraries often expose many raw exceptions, depending on how internal HTTP or retry logic is implemented. This forces library consumers to guess which exceptions to catch and creates unstable behavior.

Cause

Exception strategy is not treated as part of the library’s public contract. Internal exceptions leak out, and any change in handlers or retry logic changes what callers experience.

Resolution

Define a clear exception boundary:

  1. Internally
    Catch relevant exceptions (HttpRequestException, timeout exceptions, retry exceptions).

  2. Log them
    Use the unified logging method.

  3. Expose only a custom exception
    Throw a single exception type, such as ServiceClientException, at the public boundary.

Code Example

catch (Exception ex)
{
    LogServiceException(ex);
    throw new ServiceClientException("Service request failed.", ex);
}

This approach creates a predictable public API, hides implementation details, and ensures your library remains stable even as the internal HTTP pipeline evolves.

Comments