Click here to Skip to main content
15,880,392 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
When I use the following experimental code:
C#
var client = new HttpClient();

Task<HttpResponseMessage> response = client.GetAsync(url);
response.WaitAsync(TimeSpan.FromMilliseconds(500));

Console.WriteLine("Done async action. response.IsCompleted: {0}, and status code is {1}, task status: {2}", response.IsCompleted, response.Result.StatusCode, response.Status);

The resulting log line gives me:
Terminal
Done async action. response.IsCompleted: False, and status code is OK, task status: RanToCompletion

So, despite the task status being RanToCompletion, the IsCompleted property is False.

What I have tried:

When I use a simple Wait, so without the timeout, I do get the expected result where IsCompleted is set to True:
C#
var client = new HttpClient();
Task<HttpResponseMessage> response = client.GetAsync(url);

response.Wait();

Resulting log line:
Terminal
Done async action. response.IsCompleted: True, and status code is OK, task status: RanToCompletion


I know that I can use the await keyword on the GetAsync method but, as said, this is experimental code.


Another thing that gives me the expected result, is when I introduce a semaphore:
C#
Semaphore sem = new Semaphore(0, 1);

var client = new HttpClient();
Task<HttpResponseMessage> response = client.GetAsync(url);
response.GetAwaiter().OnCompleted(() => { sem.Release(); });

response.WaitAsync(TimeSpan.FromMilliseconds(500));
sem.WaitOne(1000);


It is almost as if there's a threading issue, because when I duplicate the log line, the second log line does show True. Like this:
Terminal
Done async action. response.IsCompleted: False, and status code is OK, task status: RanToCompletion
Done async action. response.IsCompleted: True, and status code is OK, task status: RanToCompletion


Please note that this is just experimental code, so not important at all. But I was just wondering if there's anyone out there who knows what's going on and what I might be doing wrong.
Posted
Updated 23-Feb-23 2:57am
v2

There are a few issues here.

Initializing the HttpClient this way is slow and will drain your resources quickly.

You should be using the IHttpClentFactory. The Factory will keep and recycle instances for you resulting in fast substantiation and low resource usage. You can read more here: Use IHttpClientFactory to implement resilient HTTP requests | Microsoft Learn[^]

I'll show how to do it without using DI (Dependency Injection) via IOC (Inversion of Control) container:
C#
private readonly IHttpClientFactory _httpClientFactory;

Setting up the IHttpClientFactory:
C#
ServiceCollection builder = new ServiceCollection();
builder.AddHttpClient(HttpClientKey);
ServiceProvider serviceProvider = builder.BuildServiceProvider();

_httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();

This should be in a static class (singleton design pattern) for sharing application-wide.

Then to get a HttpClient instance:
C#
private HttpClient HttpClientFactory(Uri? uri = null)
{
    HttpClient client = _httpClientFactory.CreateClient(HttpClientKey);

    if (uri is not null)
        client.BaseAddress = uri;

    return client;
}


Then to use:
C#
public async Task MyMethod()
{
    using HttpClient client = HttpClientFactory();
    {
    var response = await client.GetAsync(uri).ConfigureAwait(false);
    // do stuff here...
    }
}

Note that I have wrapped the HttpClient in a Using statement. This is so the HttpClient is correctly disposed of.

.ConfigureAwait(false) is optional however will ensure that the MyMethod will be marshaled to a thread from the threadpool away from the calling thread (most likely the UI Thread) and not lock up the application.
 
Share this answer
 
v2
Comments
Fred van Lieshout 21-Feb-23 10:25am    
Thanks for the quick reply and the suggested improvements.

The weird thing is that it all works when using the 'await' keyword, which obviously is the recommended way to go. I was just surprised to see the 'IsCompleted' property not being set to 'True', while the status of the task is actually 'RanToCompletion'. It just doesn't add up.
Graeme_Grant 21-Feb-23 10:29am    
I have not run your code to see why. Sorry, but it is far from ideal under any circumstance. Your best bet is to do it correctly, as designed, then you won't get into these situations where you will be wasting time.
Fred van Lieshout 21-Feb-23 10:32am    
Fair point!
Graeme_Grant 21-Feb-23 10:38am    
Hopefully, there is enough here to get you started, and the link provided should give you a better understanding of what you can do. I have only given you the bare minimum - the quick and dirty code that I use for small projects.
Fred van Lieshout 21-Feb-23 10:48am    
Your feedback is very much appreciated. Thanks!

You need to await the response.WaitAsync(TimeSpan.FromMilliseconds(500)) method within a try block so that you can catch the exception that the method may throw. My guess is that the response.IsCompleted is false because the WaitAsync timed out. You will not catch the exception unless you await it. Try something along these lines:


C#
private static async Task Main(string[] args)
   {
    using   var client = new HttpClient();
       string url = $"https://www.codeproject.com/";
      Task<HttpResponseMessage> response =client.GetAsync(url);
       try
       {
         await  response.WaitAsync(TimeSpan.FromMilliseconds(500));
       }
       catch (Exception ex)
       {
           Console.WriteLine(ex.Message);
       }

       Console.WriteLine("Done async action. response.IsCompleted: {0}, and status code is {1}, task status: {2}", response.IsCompleted, response.Result.StatusCode, response.Status);
 
Share this answer
 
Comments
Fred van Lieshout 23-Feb-23 8:05am    
Thanks! The thing is, it does not time out. I would have seen the exception then. I suspect there's an issue with the underlying code. Despite the documentation saying that it is thread safe, it appears that it is not (at least not 100%). The Status property is set to 'RanToCompletion', but the IsCompleted is 'false'. Until I log it again, then it is true. It is also logged immediately as 'true' when I log log it from the same thread context, i.e. in the OnCompleted callback. But never mind, I was just experimenting a bit.
George Swan 23-Feb-23 8:49am    
Thanks for your comments. The example that I posted does throw the exception and the response is flagged as not completed. If the time is changed to 1 second there are no exceptions and the response is completed but, as you state, it could be due to underlying issues in your example case.
Quote:
The Status property is set to 'RanToCompletion', but the IsCompleted is 'false'. Until I log it again, then it is true.
It's not really anything to do with the WaitAsync call; it's the Console.WriteLine call that's the problem.

If you decompile the IL of your method[^], you'll see:
IL
ldstr "Done async action. response.IsCompleted: {0}, and status code is {1}, task status: {2}"
ldloc.1

callvirt instance bool [System.Runtime]System.Threading.Tasks.Task::get_IsCompleted()
box [System.Runtime]System.Boolean
ldloc.1

callvirt instance !0 class [System.Runtime]System.Threading.Tasks.Task`1<class [System.Net.Http]System.Net.Http.HttpResponseMessage>::get_Result()
callvirt instance valuetype [System.Net.Primitives]System.Net.HttpStatusCode [System.Net.Http]System.Net.Http.HttpResponseMessage::get_StatusCode()
box [System.Net.Primitives]System.Net.HttpStatusCode
ldloc.1

callvirt instance valuetype [System.Runtime]System.Threading.Tasks.TaskStatus [System.Runtime]System.Threading.Tasks.Task::get_Status()
box [System.Runtime]System.Threading.Tasks.TaskStatus

call void [System.Console]System.Console::WriteLine(string, object, object, object)

Converting that back to low-level C# gives:
C#
string message = "Done async action. response.IsCompleted: {0}, and status code is {1}, task status: {2}";

object param1 = response.IsCompleted;

HttpResponseMessage httpResponse = response.Result;
object param2 = httpResponse.StatusCode;

object param2 = response.Status;

Console.WriteLine(message, param1, param2, param3);

As you can see, each property is accessed separately. Whilst each property access may be thread-safe, the state of the Task you're accessing can and will change between property accesses.

Also bear in mind that the Task's Result property[^] blocks the current thread until the task has completed.

So if the original task hasn't completed within 500ms:
C#
object param1 = response.IsCompleted; // === false

HttpResponseMessage httpResponse = response.Result; // Blocks the thread until the task completes...
object param2 = httpResponse.StatusCode; // At this point, the task has completed; response.IsCompleted will be true!

You can demonstrate that by changing the order of your message parameters. Try:
C#
Console.WriteLine("Done async action. response.IsCompleted: {1}, and status code is {0}, task status: {2}", response.Result.StatusCode, response.IsCompleted, response.Status);
and:
C#
Console.WriteLine("Done async action. response.IsCompleted: {0}, and status code is {2}, task status: {1}", response.IsCompleted, response.Status, response.Result.StatusCode);
 
Share this answer
 
Comments
Fred van Lieshout 24-Feb-23 4:41am    
Excellent explanation, thank you so much! I'm a newbie to C#, hence my experiment with this. Did not expect this at all, but definitely something to keep in mind!

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900