Exception handling is crucial when working with HttpClient
in C# because network operations can fail for various reasons. This guide covers the common exceptions and proper handling strategies.
Common HttpClient Exceptions
1. HttpRequestException
Thrown for network-related errors, DNS failures, and connection issues:
using System;
using System.Net.Http;
using System.Threading.Tasks;
public async Task HandleHttpRequestException()
{
using var client = new HttpClient();
try
{
var response = await client.GetAsync("https://nonexistent-domain.com");
var content = await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Network error: {ex.Message}");
// Check inner exception for more details
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
}
}
2. TaskCanceledException
Thrown when requests are canceled or timeout:
public async Task HandleTimeouts()
{
using var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(5); // 5-second timeout
try
{
var response = await client.GetAsync("https://httpbin.org/delay/10");
}
catch (TaskCanceledException ex)
{
if (ex.CancellationToken.IsCancellationRequested)
{
Console.WriteLine("Request was explicitly canceled");
}
else
{
Console.WriteLine("Request timed out");
}
}
}
3. ArgumentException
Thrown for invalid URIs or parameters:
public async Task HandleInvalidUri()
{
using var client = new HttpClient();
try
{
var response = await client.GetAsync("invalid-uri");
}
catch (ArgumentException ex)
{
Console.WriteLine($"Invalid URI: {ex.Message}");
}
catch (UriFormatException ex)
{
Console.WriteLine($"Malformed URI: {ex.Message}");
}
}
Comprehensive Exception Handling Pattern
Here's a robust exception handling pattern that covers all common scenarios:
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class HttpClientService
{
private readonly HttpClient _httpClient;
public HttpClientService()
{
_httpClient = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(30)
};
}
public async Task<string> GetDataAsync(string url, CancellationToken cancellationToken = default)
{
try
{
using var response = await _httpClient.GetAsync(url, cancellationToken);
// Handle HTTP status codes
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync();
}
// Handle specific HTTP status codes
switch (response.StatusCode)
{
case HttpStatusCode.NotFound:
throw new InvalidOperationException("Resource not found");
case HttpStatusCode.Unauthorized:
throw new UnauthorizedAccessException("Authentication required");
case HttpStatusCode.Forbidden:
throw new UnauthorizedAccessException("Access forbidden");
case HttpStatusCode.InternalServerError:
throw new InvalidOperationException("Server error occurred");
default:
throw new HttpRequestException($"HTTP error: {response.StatusCode} - {response.ReasonPhrase}");
}
}
catch (HttpRequestException ex)
{
// Network connectivity issues, DNS failures, server unreachable
throw new InvalidOperationException($"Network error occurred: {ex.Message}", ex);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
// Request timeout
throw new TimeoutException("Request timed out", ex);
}
catch (TaskCanceledException ex) when (cancellationToken.IsCancellationRequested)
{
// Explicit cancellation
throw new OperationCanceledException("Request was canceled", ex, cancellationToken);
}
catch (ArgumentException ex)
{
// Invalid URI or parameters
throw new ArgumentException($"Invalid request parameters: {ex.Message}", ex);
}
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
Using EnsureSuccessStatusCode()
For simpler scenarios, you can use EnsureSuccessStatusCode()
to automatically throw exceptions for non-success status codes:
public async Task<string> GetDataWithEnsureSuccess(string url)
{
using var client = new HttpClient();
try
{
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode(); // Throws HttpRequestException for non-success codes
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
// This will catch both network errors and HTTP error status codes
Console.WriteLine($"Request failed: {ex.Message}");
throw;
}
}
Retry Logic with Exception Handling
Implement retry logic for transient failures:
public async Task<string> GetDataWithRetry(string url, int maxRetries = 3)
{
using var client = new HttpClient();
var attempt = 0;
while (attempt < maxRetries)
{
try
{
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex) when (attempt < maxRetries - 1)
{
attempt++;
Console.WriteLine($"Attempt {attempt} failed: {ex.Message}. Retrying...");
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // Exponential backoff
}
catch (TaskCanceledException ex) when (!ex.CancellationToken.IsCancellationRequested && attempt < maxRetries - 1)
{
// Retry on timeout
attempt++;
Console.WriteLine($"Timeout on attempt {attempt}. Retrying...");
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
}
}
throw new InvalidOperationException($"Failed to get data after {maxRetries} attempts");
}
Best Practices
- Always use async/await for HttpClient operations to avoid blocking threads
- Reuse HttpClient instances rather than creating new ones for each request
- Set appropriate timeouts to prevent hanging requests
- Handle specific exceptions rather than using generic catch-all blocks
- Implement retry logic for transient failures
- Log exceptions with sufficient detail for debugging
- Use cancellation tokens for long-running operations
- Dispose HttpClient properly or use dependency injection with IHttpClientFactory
Remember that proper exception handling makes your applications more robust and provides better user experience when network issues occur.