Sitemap

Something need to caution while you invoke HttpClient inside C# code

Combined with retry policy (Polly’s) of HttpClient

3 min readJun 9, 2025

--

Press enter or click to view image in full size
Photo from ChatGPT

We use HttpClient to send requests and receive responses for data communication and exchange between internal and external applications. To ensure reliability, we have implemented a retry policy to handle network or I/O issues that may occur during runtime. The code snippet is as follows.

// Configure HttpClient with extended overall timeout
builder.Services.AddHttpClient("MyClient", client =>
{
client.Timeout = TimeSpan.FromSeconds(10);
}).AddPolicyHandler(combinedPolicy);


// Per-attempt timeout (e.g., 2 seconds per try)
var timeoutPerAttempt = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(2),
TimeoutStrategy.Optimistic
);

// Retry policy with delays
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TimeoutRejectedException>() // Catch Polly timeouts
.Or<TaskCanceledException>() // Catch HttpClient timeouts
.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(3));

// Combine policies (timeout wraps retry)
var combinedPolicy = Policy.WrapAsync(retryPolicy, timeoutPerAttempt);

The below code snippet handle the request / response:

 // Model to retrieve back the http resonpse
public class ResultModel
{
public string Status { get; set; } = string.Empty;
public string? Data { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}

// Send Post/Get http request
private async Task<ResultModel> SendAsync(HttpMethod httpMethod, string url, dynamic? jsonObject, string functionName, string version, string apiKey = "")
{
dynamic? postContent = null;
if (jsonObject != null) postContent = new StringContent(jsonObject, Encoding.UTF8, "application/json");

var result = new ResultModel();

try
{
var httpClient = _httpClientFactory.CreateClient("MyClient");
// Setup header
httpClient.DefaultRequestHeaders.Add("cache-control", "no-cache");
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.DefaultRequestHeaders.Add("api-version", version);

if (!string.IsNullOrEmpty(apiKey)) httpClient.DefaultRequestHeaders.Add("X-Api-Key", apiKey);

await httpClient
.SendAsync(new HttpRequestMessage(httpMethod, new Uri($"{url}")) { Content = postContent })
.ContinueWith(async task =>
{
if (task.IsFaulted)
{
result.Status = "Error";
result.Data = null;
result.Message = string.Join(",", task.Exception.Flatten().InnerException);
}

if (task.IsCompletedSuccessfully)
{
var response = await task;
var content = await response.Content.ReadAsStringAsync();

if (!response.IsSuccessStatusCode)
{

result.Status = "Error";
result.Data = null;
result.Message = content;
}
else
{
result.Status = "Success";
result.Data = content;
result.Message = string.Empty;
}

}

});

}
catch (HttpRequestException ex)
{
result.Status = "Error";
result.Data = null;
result.Message = $"{ex.Message}, {ex.StackTrace}";
return result;
throw;
}
catch (Exception ex)
{
result.Status = "Error";
result.Data = null;
result.Message = $"[{functionName}], {ex.Message}, {ex.StackTrace}";
return result;
throw;
}

return result.Status is "Error" or "" ? throw new ApplicationException(result.Message ?? "Task Cancelled.") : result;
}

Everything looks fine, but the HTTP request task may be canceled unexpectedly, which means we might not receive any HTTP response message.

Here is the explanation:

  • The retry policy waits 5 seconds per attempt (3 seconds delay + 2 seconds timeout).
  • Each HTTP request is retried up to 3 times.
  • The total duration for all retries is 3 × 5 = 15 seconds.
  • If HttpClient.Timeout is set to 10 seconds, which is less than 15 seconds, there is a risk that the task will be canceled before all retries are completed.

Therefore, to allow all retries to finish and to receive the HTTP response message, HttpClient.Timeout should be set to greater than 15 seconds.

Reference

--

--

LAI TOCA
LAI TOCA

Written by LAI TOCA

Coding for fun. (Either you are running for food or running for being food.)

Responses (2)