超时Web Api请求?

22

像MVC一样,WebApi也在异步ASP.NET管道上运行,这意味着不支持执行超时。

在MVC中,我使用[AsyncTimeout]过滤器,但WebApi没有这个功能。那么我该如何在WebApi中设置请求超时时间?


请查看此帖子:https://dev59.com/s2Ag5IYBdhLWcg3wZ6TY - Shafqat Masood
4个回答

22

在 Mendhak 的建议基础上,可以做到您想要的,尽管不完全是您想要的方式并需要绕过一些障碍。如果不使用过滤器,可能看起来像这样:

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 );
    }
}

在这里,由于实际上我们要做的工作需要比超时时间更长,所以您将会收到一个超时。

对于属性进行操作是可行的,但不是理想的。这与之前的基本想法相同,但是过滤器可以通过反射来执行动作。我认为我不会推荐这种方法,但在这个虚构的例子中,您可以看到如何完成它:

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 );
    }
}

那么它可以像这样使用:

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

这适用于简单类型(例如字符串),在Firefox中可以得到:<z:anyType i:type =“d1p1:string”>Results</z:anyType>,尽管如您所见,序列化并不理想。使用此完全相同的代码与自定义类型将会有一些问题,就序列化而言,但经过一些工作,这可能在某些特定情况下是有用的。由于操作参数采用字典形式而不是数组形式,这也可能导致某些参数排序方面的问题。显然,真正的支持会更好。

至于vNext方面的内容,他们可能计划添加Web API的服务器端超时功能,因为MVC和API控制器正在统一。如果他们这样做了,很可能不会通过System.Web.Mvc.AsyncTimeoutAttribute类实现,因为他们正在明确删除对System.Web的依赖。

截至今天,看起来向project.json文件添加System.Web.Mvc条目不起作用,但这可能会改变。如果确实是这样,虽然您无法在这种代码中使用新的云优化框架,但您可能仍然可以在只打算在完整的.NET框架上运行的代码上使用AsyncTimeout属性。

值得一提的是,这是我尝试添加到project.json的内容。也许特定版本会让它更开心?

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

在“解决方案资源管理器”的引用列表中确实会出现对它的引用,但是会伴随着一个黄色的感叹号表示存在问题。只要这个引用还在,应用程序本身就会返回 500 错误。


5
请问downvoter可以解释一下为什么要踩吗? +1 这是一个非常详尽的概述。 - Carrie Kendall
我尝试了这种方式,但不是理想的解决方案,因为它只适用于同步操作,在上面的示例中,动作是Thread.Sleep(5000),如果将其更改为await Task.Delay(5000),那么它不会超时,因为它是异步的,不是一个非常通用的解决方案 :( - user1108069

10

在WebAPI中,一般你会在客户端处理超时问题,而不是在服务器端。因为,正如我引用的话

取消HTTP请求的方法是直接在HttpClient上取消它们。原因是,在单个HttpClient中,多个请求可以重用TCP连接,因此您不能安全地取消单个请求而不可能影响其他请求。

您可以控制请求的超时时间--如果我没记错的话,它在HttpClientHandler上。

如果你真的需要在API端本身实现超时,我建议创建一个线程来做你的工作,然后在一定时间后取消它。例如,你可以将它放在一个Task中,使用Task.Wait创建你的“超时”任务,并对第一个返回的任务使用Task.WaitAny。这样就可以模拟超时了。

同样,如果你正在执行特定的操作,请检查它是否已经支持超时。很多时候,我会从我的WebAPI中执行一个HttpWebRequest并指定它的Timeout属性。


抱歉,这并没有真正回答我的问题。我的 API 被浏览器中的 JavaScript 使用,而且没有办法启用跨浏览器工作的请求超时。这就是为什么我正在寻找 WebApi 中 [AsyncTimeout] 的等效方法,以便我可以在服务器上执行它。 - DalSoft
看到引号后面的那一部分 - 你可以使用Task.WaitAny来实现,将主任务放在一个线程中,同时在另一个线程中创建一个“等待”超时任务。如果超时任务提前返回,那么你就可以立即返回。 - Mendhak
我明白了,感谢您的回答。但是我正在寻找一种可以全局应用的解决方案,就像MVC中的[AsyncTimeout]一样。我认为这在我正在审查的vNext中得到了支持。 - DalSoft
我相信你是对的,它将在vNext DalSoft中得到支持。不仅包括[AsyncTimeout]的添加,还有许多属性我们不能像常规的ViewController一样访问。 - Nick Klufas

5

对于每个需要超时的终结点,在其中通过管道传递CancellationToken,例如:

[HttpGet]
public Task<Response> GetAsync()
{
    var tokenSource = new CancellationTokenSource(_timeoutInSec * 1000);
    return GetResponseAsync(tokenSource.Token);
}

2
在您的基本控制器中添加下面的方法,让您的生活更加轻松:
    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;
    }

现在,任何继承自您基础控制器的控制器都可以访问RunTask方法。现在,在您的API调用中,只需像这样调用RunTask方法:
    [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();
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接