Michal
Michal

Reputation: 1406

Timeout for HttpClient doesn't work inside async block

I am trying to use HttpClient to send POST request in F#. Unfortunately I have found that timeouts in F# doesn't work. I have a code like this:

let asyncTest() = async {
    let httpClient = new HttpClient()
    try 
        let! res = httpClient.PostAsync("http://135.128.223.112", new StringContent("test"), (new CancellationTokenSource(2000)).Token) |> Async.AwaitTask
        Console.WriteLine("Test 1")
    with 
        | :? Exception as e -> Console.WriteLine("Test 2")
} 

IP which you can see in code doesn't exist. I expect call to be cancelled after 2 seconds (cancellation token) but exception is never thrown ("Test 2" is never printed on screen). After PostAsync method call program control never returns into async block. Behavior is the same if I use HttpClient.Timeout property or Async.Catch instead of try block.

In C# the same code works as expected, after two seconds exception is raised:

private async Task Test()
{
    var httpClient = new HttpClient();
    await
            httpClient.PostAsync("http://135.128.223.112", new StringContent("test"),
                new CancellationTokenSource(2000).Token);
 }

I know that async in F# and C# is different so I expect this be caused by some synchronization problem, deadlock ... I hope someone with deeper understanding of F# async can explain this and provide workaround.

UPDATE: I am starting F# async work like this:

Async.Start (asyncTest())

I am running this test in console appliation and I have ReadKey() at the end so app doesn't end sooner than async.

After rading Fyodor's comment I tried to change it to RunSynchronously. Now exception is raised but still I need it work with Async.Start.

Upvotes: 3

Views: 699

Answers (1)

scrwtp
scrwtp

Reputation: 13577

What you are seeing is a consequence of two things:

  1. OperationCancelledException using a special path in F# async through the cancellation continuation. What it means in practice is that within the context of an async block it's an "uncatchable" exception, bypassing try-with, which works on the error continuation.
  2. AwaitTask integrating the task you're waiting on to be part of your async block - which includes promoting task cancellation into async cancellation (OperationCancelledException instead of TaskCancelledException) and bringing your entire workflow down.

That's not an obvious behavior and it appears that it's being changed for the future versions of F#.

In the meantime, you can try something like this instead of Async.AwaitTask:

let awaitTaskCancellable<'a> (task: Task<'a>) = 
    Async.FromContinuations(fun (cont, econt, ccont) -> 
        task.ContinueWith(fun (t:Task<'a>) -> 
            match t with
            | _ when t.IsFaulted  -> econt t.Exception
            | _ when t.IsCanceled -> 
                // note how this uses error continuation 
                // instead of cancellation continuation
                econt (new TaskCanceledException())
            | _ -> cont t.Result) |> ignore)

Upvotes: 5

Related Questions