8

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
WiseGuyEh
  • 18,584
  • 1
  • 20
  • 20
  • In general, exceptions are funny with async. You want to use `Async.Catch`. The weird behaviour is caused when the exception is rethrown on the main thread causing the odd stack trace. – John Palmer Oct 03 '14 at 11:15
  • I've tried writing this in the Async.Catch style and the problem still occurs. The other thing is that if another type of exception is thrown, it is caught correctly. I'm wondering if the Async module handles cancellation tokens might be at fault- perhaps it's misinterpreting the exception as a cancellation request to the module. – WiseGuyEh Oct 03 '14 at 11:29

1 Answers1

6

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
}
WiseGuyEh
  • 18,584
  • 1
  • 20
  • 20
desco
  • 16,642
  • 1
  • 45
  • 56