HttpClient cancellation doesn't kill underlying TCP call - c#

I'm trying to set a default timeout for my HttpClient calls to 5 seconds.
I've done this via CancellationTokenSource.
Here's the pertinent bit of code:
var cancellationToken = new CancellationTokenSource();
cancellationToken.CancelAfter(TimeSpan.FromSeconds(5));
var result = _httpClient.SendAsync(request, cancellationToken.Token);
Works as i expected in terms of the calling code getting a "Task was cancelled" error (i tested in a .NET 4.7 console app), but i noticed in Fiddler the request was still running for 1 minute, until it finally gave up:
Can someone explain this behaviour?
I would expect the underlying request to also get cancelled when the cancellation is triggered.
_httpClient is instantiated like: new HttpClient { BaseAddress = baseAddress }
I know there's the the Timeout setting, but not sure if I should be using that or cancellation tokens? My guess is Timeout is for the non-async/await cases?

As Damien said in the comments, HttpClient re-uses connections as much as possible, hence the reason why the connection is not closed on cancel.
When canceling a request like that, the HttpClient will just stop sending/receiving data to/from the other end. It will not send anything to inform the other end that it was cancelled. So the timeout you see of 1 minute depends on the behavior of the other end of your connection.
Also, if you want to cancel each request after 5 seconds, you can as well set the Timeout property of _httpClient to TimeSpan.FromSeconds(5). The behavior will be exactly the same (a TaskCanceledException will be thrown if the other end doesn't respond within 5 seconds).

If anyone is interested, you can try the following approach to applying your own timeout per HttpClient request. It seems to work for me, restricting the SendAsync() to 2 seconds and returning immediately when the timeout occurs:
private async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, TimeSpan? timeout = null)
{
if (timeout is null)
{
return await _httpClient.SendAsync(request);
}
else
{
using (var cts = new CancellationTokenSource(timeout.Value))
{
var sendTask = _httpClient.SendAsync(request);
while (!sendTask.IsCompleted)
{
cts.Token.ThrowIfCancellationRequested();
await Task.Delay(10).ConfigureAwait(false);
}
return await sendTask.ConfigureAwait(false);
}
}
}

Related

Polly Retries Doesn't Close Existing HttpClient Call

I'm using Polly to handle some scenarios like request throttled and timeouts.
The policies were added directly in the Startup.cs which would be like this :
var retries = //applying the retries, let say I set to 25 times with 10s delay. Total 250s.
serviceCollection
.AddHttpClient<IApplicationApi, ApplicationApi>()
.AddPolicyHandler((services, request) => GetRetryPolicy<ApplicationApi>(retries, services));
The Policy:
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy<T>(List<TimeSpan> retries, IServiceProvider services)
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(retries,
onRetry: (outcome, timespan, retryAttempt, context) =>
{
//do some logging
}
}
In ApplicationApi.cs do something like this:
private readonly HttpClient _httpClient;
public ApplicationApi(HttpClient httpClient)
{
_httpClient = httpClient;
}
public void CallApi()
{
var url = "https://whateverurl.com/someapi"
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
var response = await _httpClient.SendAsync(request);
var respMessage = await
response.Content.ReadAsStringAsync();
}
}
Now let say I don't specify the HttpClient.Timeout, which then will use default timeout : 100s.
Now I have a problem with heavy throttling. Polly will retry until the throttling resolved, or it reach the max retry.
But, the program will thrown an exception on the 10th retry since it already more than 100s elapsed on httpclient since the first request it got throttled.
Seems like the first http request that got throttled still on and not closed, or I may be wrong.
What causing this? Is it a normal behavior of Polly retries?
How can I make it close the connection on each retries so I don't have to set a very high HttpClient.Timeout value.
I also implemented the Polly timeout policy to cut request if more than some specified second then retry until it succeed. But the Polly behavior still like this. So I need to set httpclient timeout > total elapsed time on retries
**UPDATE
Code updated. So I just realized there's using statement for the request.
***UPDATE
I've created a repo that reproduce the behavior here : https://github.com/purnadika/PollyTestWebApi
The short answer is that your observed behaviour is due to fact how AddPolicyHandler and PolicyHttpMessageHandler work.
Whenever you register a new Typed HttpClient without any policy (.AddHttpClient) then you basically create a new HttpClient like this:
var handler = new HttpClientHandler();
var client = new HttpClient(handler);
Of course it is much more complicated, but from our topic perspective it works like that.
If you register a new Typed HttpClient with a policy (.AddHttpClient().AddPolicyHandler()) then you create a new HttpClient like this
var handler = new PolicyHttpMessageHandler(yourPolicy);
handler.InnerHandler = new HttpClientHandler();
var client = new HttpClient(handler);
So the outer handler will be the Polly's MessageHandler and the inner is the default ClientHandler.
Polly's MessageHandler has the following documentation comment:
/// <para>
/// Take care when using policies such as Retry or Timeout together as HttpClient provides its own timeout via
/// <see cref="HttpClient.Timeout"/>. When combining Retry and Timeout, <see cref="HttpClient.Timeout"/> will act as a
/// timeout across all tries; a Polly Timeout policy can be configured after a Retry policy in the configuration sequence,
/// to provide a timeout-per-try.
/// </para>
By using the AddPolicyHandler the HttpClient's Timeout will act as a global timeout.
The solution
There is workaround, namely avoiding the usage of AddPolicyHandler.
So, rather than decorating your Typed Client at the registration time you can decorate only the specific HttpClient method call inside your typed client.
Here is a simplified example based on your dummy project:
ConfigureServices
services.AddHttpClient<IApplicationApi, ApplicationApi>(client => client.Timeout = TimeSpan.FromSeconds(whateverLowValue));
_MainRequest
var response = await GetRetryPolicy().ExecuteAsync(async () => await _httpClient.GetAsync(url));
Here I would like to emphasize that you should prefer GetAsync over SendAsync since the HttpRequestMessage can not be reused.
So, if you would write the above code like this
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
var response = await GetRetryPolicy().ExecuteAsync(async () => await _httpClient.SendAsync(request));
}
then you would receive the following exception:
InvalidOperationException: The request message was already sent. Cannot send the same request message multiple times.
So, with this workaround the HttpClient's Timeout will not act as a global / overarching timeout over the retry attempts.
Polly works to retry the top-level HttpClient request, so HttpClient's timeout applies to all retries. That's the whole point of using Polly, to retry requests in a way that's transparent to the top-level code.
If retrying for over a minute failed to work, the retry policy isn't good enough. Retrying over and over with a fixed delay will only result in more 429 responses, as all the requests that failed will be retried at the same time. This will result in wave after wave of identical requests hitting the server, resulting in 429s once again.
To avoid this, exponential backoff and jitter are used to introduce an increasing random delay to retries.
From the linked sample:
var delay = Backoff.DecorrelatedJitterBackoffV2(
medianFirstRetryDelay: TimeSpan.FromSeconds(1),
retryCount: 5);
var retryPolicy = Policy
.Handle<FooException>()
.WaitAndRetryAsync(delay);
The Polly page for the Jitter strategy explain how this works. The distribution graph of delays shows that even with 5 retries, the retry intervals don't clamp together.
This means there's less chance of multiple HttpClient calls retrying at the same time, resulting in renewed throttling

HttpClient get request cancellation, in case the operation takes longer than x minutes

I am writing an azure function which synchronizes data every 15 minutes.
I am targeting .net 3.1 for azure function and I am using httpclient to send a get request to their API.
var response = await client.GetStringAsync()
It usually takes about 20 seconds to get the results, however, sometimes their server does not process the request correctly or it is just busy, and takes too long to do so or does not return any data. I've tested in postman, and I get status Ok 200, but it keeps loading for many minutes, and nothing happens.
However, if you just cancel the operation, and try again, it is responds fine.
Now My question is, how could I keep track of the time it takes for the result to come from my get client.GetStringAsync() method, and somehow cancel the operation if lets say it is longer than 2 minutes?
If you are using .NET 5, you can use a CancellationTokenSource with a 'cancel after' and pass the token to client.GetStringAsync(), like this:
var cts = new CancellationTokenSource()
cts.CancelAfter(120000); // Cancel after 2 minutes
try
{
var response = await client.GetStringAsync(uri, cts.Token);
}
catch (TaskCanceledException)
{
Console.WriteLine("\nTask cancelled due to timeout.\n");
}
Then you can wrap the operation in a try-catch block to see if the operation was cancelled by the token.
Reference: Cancel async tasks after a period of time (C#)
UPDATE
On .NET Core, you can specify a timeout for your HttpClient. Note however that this timeout applies to all requests you send using that instance of HttpClient. If you wish to do it that way, you can do as follows:
client.Timeout = TimeSpan.FromMinutes(2);
try
{
var response = await client.GetStringAsync(uri);
}
catch (TaskCanceledException)
{
Console.WriteLine("\nRequest cancelled due to timeout.\n");
}

Calls to an API fails after some time when multiple users are logged in and using the application. Is there a way to solve this?

I am making some calls to an api that syncs and saves appointments to/from my application and mail agenda which is most commonly Outlook mails. The calls to the api are made from a web application and it is working fine for some time but then immediately after a few hours, the calls are failed. This continues for some time and starts to work again after some time.
The DefaultConnectionLimit was set to 100 in the beginning and during this scenario, the process stopped working after some time (Say 30 mins to 1 hour). Then DefaultConnectionLimit was set to 20 and in this case, it worked fine for 3-4 hours and stopped working after that. I don't know if this is the actual cause but just mentioning.
The code where the call to the api is made from the web application is mentioned below :
public bool SyncNewAppToExch(string encryptedAppSyncDetails)
{
try
{
string result = string.Empty;
JArray paramList = new JArray();
paramList.Add(encryptedAppSyncDetails);
var emailApiUrl = ConfigurationManager.AppSettings["emailApiUrl"].ToString();
Uri baseAddress = new Uri(emailApiUrl);
var url = "/api/EmailProcess/ManualSaveAppointments";
HttpClient client = new HttpClient();
client.BaseAddress = baseAddress;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
Task.Run(() => client.PostAsJsonAsync(url, paramList)).Wait(5000);
var responseMessage = true;
if (responseMessage)
return true;
else
return false;
}
catch (Exception ex)
{
return false;
}
}
The exception that follows the api call failure reads :
Exception : System.AggregateException: One or more errors occurred. System.Threading.Tasks.TaskCanceledException: A task was canceled. --- End of inner exception stack trace --- at System.Threading.Tasks.Task1.GetResultCore(Boolean waitCompletionNotification) at System.Threading.Tasks.Task1.get_Result()
If you are making a new HttpClient object each time you want to make a call, you are not using it appropriately. HttpClient may continue to use sockets even if it is no longer being used and this will likely cause socket exhaustion under load. You can see more on this topic here
You should create a single instance and reuse it. If you are using .Net core it would be a better idea to use the HttpClientFactory.
In addition, using Task.Run to call an async method is not wise. You are setting yourself up for a deadlock. It would be much better to mark your method as async and await, if possible. Since you are effectively doing fire and forget, the way you have done this if your App Domain shuts down this work will be lost.
If you need to do this, you should at least consider registering your task as such:
HostingEnvironment.QueueBackgroundWorkItem(ct => SendMailAsync(user.Email));
The short answer is that it is difficult to say what your issue is, it could be one of several problems.

System.Net.HttpClient: SendAsync failed with OperationCanceledException without http request throught network

We use System.Net.Http.HttpClientfor calls between microservices inside k8s.
OS Linux(inside docker)
dotnet TargetFramework: netcoreapp2.1
server: Kestrel
protocol: http
Few days ago we noticed very strange behaviour of http calls:
some calls between microservice(near 2-3%) failed with error
System.OperationCanceledException: The operation was canceled.
at System.Net.Http.HttpClient.HandleFinishSendAsyncError(Exception e, CancellationTokenSource cts)
at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
at OurCode.HttpServiceClient.GetStringResponseAsync(String methodUri, HttpMethod httpMethod)
...another our code...
after our timeout for http calls(it is 3 sec). But there was no logs about call inside callee service.
We enabled packetbeat for tracing http requests and also noticed, that no any http requests from caller service to callee service was executed.
CPU, memory and network for this services was OK all the time.
Simplified version of our code for http calls looks like:
public async Task<string> GetStringResponseAsync(String methodUri, HttpMethod httpMethod)
{
int timeoutInMilliseconds = 3000;
var tokenSource = new CancellationTokenSource();
var rm = new HttpRequestMessage(httpMethod, methodUri);
Task<HttpResponseMessage> httpTask = HttpClient.SendAsync(rm, tokenSource.Token);
tokenSource.CancelAfter(timeoutInMilliseconds);
HttpResponseMessage response = await httpTask;
await EnsureSuccessStatusCode(response);
return await response.Content.ReadAsStringAsync();
}
Any ideas about what problem can cause this strange behaviour without http request through network, and what can I do for further investigation?
It just meant that the web service did not respond.
HttpClient throws a TaskCanceledException, (which inherits from OperationCanceledException) when the timeout elapses. It is not intuitive and doesn't make any sense to me (and others), but that's what it does unfortunately.
There is some discussion about it here (a few people mentioned some workarounds to distinguish timeout from a true cancel, if you care).
I don't know if this will help anyone in the future but I had the same issues. Turns out the server I was hosting on was not automatically using the proxy address by default. I ended up having to add the following to my code:
using (var handler = new WinHttpHandler()){
handler.WindowsProxyUsePolicy = WindowsProxyUsePolicy.UseCustomProxy;
handler.Proxy = new WebProxy(server, port);
//the rest of your code.....
}
This manually added the proxy server information and my console app began working as expected.

Dynamically changing HttpClient.Timeout in .NET

I need to change a HttpClient.Timeout property after it made a request(s). When I try, I get an exception:
This instance has already started one or more requests. Properties can only be modified before sending the first request.
Is there any way to avoid this?
There isn't much you can do to change this. This is just default behavior in the HttpClient implementation.
The Timeout property must be set before the GetRequestStream or GetResponse method is called.
From HttpClient.Timeout Remark Section
In order to change the timeout, it would be best to create a new instance of an HttpClient.
client = new HttpClient();
client.Timeout = 20; //set new timeout
Internally the Timeout property is used to set up a CancellationTokenSource which will abort the async operation when that timeout is reached. Since some overloads of the HttpClient methods accept CancellationTokens, we can create helper methods to have a custom timeouts for specific operations:
public async Task<string> GetStringAsync(string requestUri, TimeSpan timeout)
{
using (var cts = new CancellationTokenSource(timeout))
{
HttpResponseMessage response = await _httpClient.GetAsync(requestUri, cts.Token)
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
Lack of support for custom request-level timeouts has always been a shortcoming of HttpClient in my mind. If you don't mind a small library dependency, Flurl.Http [disclaimer: I'm the author] supports this directly:
"http://api.com/endpoint".WithTimeout(30).GetJsonAsync<T>();
This is a true request-level setting; all calls to the same host use a shared HttpClient instance under the hood, and concurrent calls with different timeouts will not conflict. There's a configurable global default (100 seconds initially, same as HttpClient).

Categories