22

Like MVC WebApi runs on the asynchronous ASP.NET pipeline, meaning execution timeout is unsupported.

In MVC I use the [AsyncTimeout] filter, WebApi doesn't have this. So how do I timeout a request in WebApi?

DalSoft
  • 10,673
  • 3
  • 42
  • 55

4 Answers4

22

Building on the suggestion by Mendhak, it is possible to do what you want, though not exactly the way you'd like to do it without jumping through quite a few hoops. Doing it without a filter might look something like this:

public class ValuesController : ApiController
{
    public async Task<HttpResponseMessage> Get( )
    {
        var work    = this.ActualWork( 5000 );
        var timeout = this.Timeout( 2000 );

        var finishedTask = await Task.WhenAny( timeout, work );
        if( finishedTask == timeout )
        {
            return this.Request.CreateResponse( HttpStatusCode.RequestTimeout );
        }
        else
        {
            return this.Request.CreateResponse( HttpStatusCode.OK, work.Result );
        }
    }

    private async Task<string> ActualWork( int sleepTime )
    {
        await Task.Delay( sleepTime );
        return "work results";
    }

    private async Task Timeout( int timeoutValue )
    {
        await Task.Delay( timeoutValue );
    }
}

Here you will receive a timeout because the actual "work" we're doing will take longer than the timeout.

To do what you want with an attribute is possible, though not ideal. It's the same basic idea as before, but the filter could actually be used to execute the action via reflection. I don't think I would recommend this route, but in this contrived example, you can see how it might be done:

public class TimeoutFilter : ActionFilterAttribute
{
    public int Timeout { get; set; }

    public TimeoutFilter( )
    {
        this.Timeout = int.MaxValue;
    }
    public TimeoutFilter( int timeout )
    {
        this.Timeout = timeout;
    }


    public override async Task OnActionExecutingAsync( HttpActionContext actionContext, CancellationToken cancellationToken )
    {

        var     controller     = actionContext.ControllerContext.Controller;
        var     controllerType = controller.GetType( );
        var     action         = controllerType.GetMethod( actionContext.ActionDescriptor.ActionName );
        var     tokenSource    = new CancellationTokenSource( );
        var     timeout        = this.TimeoutTask( this.Timeout );
        object result          = null;

        var work = Task.Run( ( ) =>
                             {
                                 result = action.Invoke( controller, actionContext.ActionArguments.Values.ToArray( ) );
                             }, tokenSource.Token );

        var finishedTask = await Task.WhenAny( timeout, work );

        if( finishedTask == timeout )
        {
            tokenSource.Cancel( );
            actionContext.Response = actionContext.Request.CreateResponse( HttpStatusCode.RequestTimeout );
        }
        else
        {
            actionContext.Response = actionContext.Request.CreateResponse( HttpStatusCode.OK, result );
        }
    }

    private async Task TimeoutTask( int timeoutValue )
    {
        await Task.Delay( timeoutValue );
    }
}

This could then be used like this:

[TimeoutFilter( 10000 )]
public string Get( )
{
    Thread.Sleep( 5000 );
    return "Results";
}

This works for simple types (e.g. string), giving us: <z:anyType i:type="d1p1:string">Results</z:anyType> in Firefox, though as you can see, the serialization is not ideal. Using custom types with this exact code will be a bit problematic as far as serialization goes, but with some work, this could probably be useful in some specific scenarios. That the action parameters come in the form of a dictionary instead of an array could also pose some issues in terms of the parameter ordering. Obviously having real support for this would be better.

As far as the vNext stuff goes, they may well be planning to add the ability to do server-side timeouts for Web API since MVC and API controllers are being unified. If they do, it will likely not be through the System.Web.Mvc.AsyncTimeoutAttribute class, as they are explicitly removing dependencies on System.Web.

As of today, it doesn't appear that adding a System.Web.Mvc entry to the project.json file works, but this may well change. If it does, while you wouldn't be able to use the new cloud-optimized framework with such code, you might be able to use the AsyncTimeout attribute on code that is only intended to run with the full .NET framework.

For what it's worth, this is what I tried adding to project.json. Perhaps a specific version would have made it happier?

"frameworks": {
    "net451": {
        "dependencies": { 
            "System.Web.Mvc": ""
        }
    }
}

A reference to it does show up in the Solution Explorer's references list, but it does so with a yellow exclamation point indicating a problem. The application itself returns 500 errors while this reference remains.

Adam
  • 621
  • 2
  • 9
  • 11
  • 5
    Would the downvoter care to explain why? +1 This is an incredibly thorough overview. – Carrie Kendall Sep 22 '14 at 22:37
  • I tried this way but not ideal because it works for synchronous action, in the above example, in action it's `Thread.Sleep(5000)`, if change it to `await Task.Delay(5000)`then it doesn't timeout because it's async, not a quite general solution :( – user1108069 Jun 09 '22 at 09:52
10

With WebAPI, you would generally handle timeouts on the client side, rather than the server side. This is because, and I quote:

The way to cancel HTTP requests is to cancel them on the HttpClient directly. The reason being that multiple requests can reuse TCP connections within a single HttpClient and so you can't safely cancel a single request without possibly affecting other requests as well.

You can control the timeout for requests -- I think it's on the HttpClientHandler if I recall correctly.

If you really need to implement a timeout on the API side itself, I would recommend creating a thread to do your work in, and then cancelling it after a certain period. You could for example put it in a Task, create your 'timeout' task using Task.Wait and use Task.WaitAny for the first one to come back. This can simulate a timeout.

Similarly, if you are performing a specific operation, check to see if it already supports timeouts. Quite often, I will perform an HttpWebRequest from my WebAPI, and specify its Timeout property.

Community
  • 1
  • 1
Mendhak
  • 8,194
  • 5
  • 47
  • 64
  • Sorry this doesn't really answer my question. My Api is consumed by javascript in the browser and there is no way to enable a request timeout that works cross browser. This is why I'm looking for the WebApi equivalent of [AsyncTimeout] so I can do it on the server. – DalSoft Jun 25 '14 at 12:51
  • See the bit after the quote - you can do it using `Task.WaitAny`, with your main task in one thread and a 'wait' timeout task in another thread. If the timeout task returns earlier, then you can return immediately. – Mendhak Jun 25 '14 at 15:50
  • I understand thanks for the answer. But what I'm looking for is a solution I can apply globally like [AsyncTimeout] in MVC. I think it is supported in vNext which I'm reviewing. – DalSoft Jun 26 '14 at 10:19
  • I believe you're right that it will be supported in vNext DalSoft. Not only the [AsyncTimeout] addition but also many attributes we don't have access to when we compare against a regular ViewController – Nick Klufas Aug 21 '14 at 20:56
5

For each endpoint where you want a timeout, pipe a CancellationToken through, e.g.:

[HttpGet]
public Task<Response> GetAsync()
{
    var tokenSource = new CancellationTokenSource(_timeoutInSec * 1000);
    return GetResponseAsync(tokenSource.Token);
}
Hans Vonn
  • 3,949
  • 3
  • 21
  • 15
2

Make your life easier, in your base controller add the following method:

    protected async Task<T> RunTask<T>(Task<T> action, int timeout) {
        var timeoutTask = Task.Delay(timeout);
        var firstTaskFinished = await Task.WhenAny(timeoutTask, action);

        if (firstTaskFinished == timeoutTask) {
            throw new Exception("Timeout");
        }

        return action.Result;
    }

Now every controller that inherits from your base controller can access the method RunTask. Now in your API call the RunTask method just like that:

    [HttpPost]
    public async Task<ResponseModel> MyAPI(RequestModel request) {
        try {
            return await RunTask(Action(), Timeout);
        } catch (Exception e) {
            return null;
        }
    }

    private async Task<ResponseModel> Action() {
        return new ResponseModel();
    }
Daniel Dantas
  • 371
  • 3
  • 5