Search code examples
f#task-parallel-librarycomputation-expressionasync-workflow

async computation doesn't catch OperationCancelledException


I'm trying to make an asynchronous web request to a URL that will return if the request takes too long. I'm using the F# asynchronous workflow and the System.Net.Http library to do this.

However, I am unable to catch the Task/OperationCancelledExceptions that are raised by the System.Net.Http library in the async workflow. Instead, the exception is raised at the Async.RunSynchronously method, as you can see in this stack trace:

> System.OperationCanceledException: The operation was canceled.    at
> Microsoft.FSharp.Control.AsyncBuilderImpl.commit[a](Result`1 res)   
> at
> Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously[a](CancellationToken
> token, FSharpAsync`1 computation, FSharpOption`1 timeout)    at
> Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1
> computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
> at <StartupCode$FSI_0004>.$FSI_0004.main@()

The code:

#r "System.Net.Http"

open System.Net.Http
open System

let readGoogle () = async {
    try
        let request = new HttpRequestMessage(HttpMethod.Get, "https://google.co.uk")
        let client = new HttpClient()
        client.Timeout <- TimeSpan.FromSeconds(0.01) //intentionally low to always fail in this example
        let! response = client.SendAsync(request, HttpCompletionOption.ResponseContentRead) |> Async.AwaitTask 
        return Some response
    with 
        | ex ->
            //is never called
            printfn "TIMED OUT" 
            return None
}

//exception is raised here
readGoogle ()
    |> Async.RunSynchronously
    |> ignore

Solution

  • Cancellation was always different from the error. In your case you can override default behavior of AwaitTask that invokes "cancel continuation" if task is cancelled and handle it differently:

    let readGoogle () = async {
        try
            let request = new HttpRequestMessage(HttpMethod.Get, "https://google.co.uk")
            let client = new HttpClient()
            client.Timeout <- TimeSpan.FromSeconds(0.01) //intentionally low to always fail in this example
            return! ( 
                let t = client.SendAsync(request, HttpCompletionOption.ResponseContentRead)
                Async.FromContinuations(fun (s, e, _) ->
                    t.ContinueWith(fun (t: Task<_>) -> 
                        // if task is cancelled treat it as timeout and process on success path
                        if t.IsCanceled then s(None)
                        elif t.IsFaulted then e(t.Exception)
                        else s(Some t.Result)
                    )
                    |> ignore
                )
            )
        with 
            | ex ->
                //is never called
                printfn "TIMED OUT" 
                return None
    }