ASP.Net Core中间件无法在异常时设置状态代码,因为“响应已经开始”

时间:2017-08-14 12:59:44

标签: c# .net asp.net-core

相关:Modify static file response in ASP.NET Core

但是,当我的业务逻辑抛出我的一个自定义异常,例如UnprocessableException时,我不明白为什么以下代码有效:

try
{
    await next.Invoke(context);
}
catch (UnprocessableException uex)
{
    Logger.Warn(uex);
    context.Response.StatusCode = 422;
    var responseContent = JsonConvert.SerializeObject(new { uex.Message });
    await context.Response.WriteAsync(responseContent);
}
// more specific exceptions resulting in HTTP 4xx status

但是当链中的最后一个IndexOutOfRangeException块捕获到完全意外的catch

catch (Exception ex)
{
    Logger.Error(ex);
    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
    var responseContent = env.IsDevelopment()
                              ? JsonConvert.SerializeObject(new { ex.Message, ex.StackTrace })
                              : JsonConvert.SerializeObject(new { Message = "An internal error occured" });
    await context.Response.WriteAsync(responseContent);
}

尝试设置状态代码时抛出此异常:

System.InvalidOperationException: StatusCode cannot be set, response has already started.
   bei Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame.ThrowResponseAlreadyStartedException(String value)
   bei Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame.set_StatusCode(Int32 value)
   bei Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value)
   bei Microsoft.AspNetCore.Http.Internal.DefaultHttpResponse.set_StatusCode(Int32 value)
   bei Anicors.Infrastructure.Middlewares.ScopeMiddleware.<Invoke>d__5.MoveNext()

6 个答案:

答案 0 :(得分:3)

这里只是个问题:我从处理WebSocket连接的控制器收到此错误。当关闭WebSocket连接(用户关闭浏览器选项卡)时,抛出此异常:System.InvalidOperationException: StatusCode cannot be set because the response has already started.还请注意,在堆栈跟踪中找不到用于处理WebSocket连接的控制器:

System.InvalidOperationException: StatusCode cannot be set because the response has already started.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value)
   at Microsoft.AspNetCore.Http.Internal.DefaultHttpResponse.set_StatusCode(Int32 value)
   at Microsoft.AspNetCore.Mvc.StatusCodeResult.ExecuteResult(ActionContext context)
   at Microsoft.AspNetCore.Mvc.ActionResult.ExecuteResultAsync(ActionContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultAsync(IActionResult result)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResultFilterAsync[TFilter,TFilterAsync]()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResultExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultFilters()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.Invoke(HttpContext context)
   at MyApp.Middleware.MyAppNotFoundHandlerMiddleware.Invoke(HttpContext context) in C:\Proj\MyApp\Middleware\MyAppNotFoundHandlerMiddleware.cs:line 24
   at MyApp.Middleware.MyAppExceptionHandlerMiddleware.Invoke(HttpContext context) in C:\Proj\MyApp\Middleware\MyAppExceptionHandlerMiddleware.cs:line 26
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

这是控制器操作出错的地方:

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Get()
{
    if (HttpContext.WebSockets.IsWebSocketRequest)
    {
        var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();
        Clients.Add(socket);
        await WaitForClose(HttpContext, socket);
    }
    return Ok();
}

正如其他答案所述,罪魁祸首是return Ok()。该语句在套接字关闭时执行,但是到那时,HTTP连接早已关闭。

我正在使用2.1.0版的NuGet软件包Microsoft.AspNetCore.WebSockets

答案 1 :(得分:2)

哦,好吧,我正在进一步调查,同时试图重现案件更加孤立,我找到了根本原因。

但首先是一些历史:我在生产中看到了这些错误,但从未能够重现它。现在我正在开发另一个功能,由于我的开发机器上的数据库结构出错,这个错误发生在使用正常连接查询的每个请求上。所以我想,嘿,这是解决这个问题的时刻......但最终还是来了。

然而,为了更多地隔离它,我做了一个动作只是在我脸上扔了NotImplementedException。并猜测:它按预期工作。 HTTP 500,没有“无法设置StatusCode,响应已经开始”。

有什么区别?不同的是,我的另一个失败的控制器返回:

IQueryable<MySearchRecords> searchResult = service.Search(/*snipped boring stuff*/);
var result = DataSourceLoader.Load(searchResult, loadOptions);
return Ok(result);

而DataSourceLoader是一个.net类,支持DevExpress的DevExtreme JS Framework。事实证明,结果是object,因为它返回一个普通数组或一个包含类型,它也提供了一些元数据(例如,用于分页和填充)。在我的情况下,它会应用一些TakeSkip,但是:不会枚举搜索结果,但会返回IQueryable<>!因此,枚举不会早于将结果呈现给JSON时完成。这就是我在这种特殊情况下看到InvalidOperationException的原因,但不是直接从控制器抛出它的原因。

尽管如此,它表明我的异常处理在所有情况下都没有按预期工作。我已经读过你可以替换整个响应流来避免这个问题,但这有一些缺点。那么处理这种情况的正确方法是什么呢?无论如何,我想要使用自定义JSON内容的HTTP 500。

答案 2 :(得分:2)

由于这是Google上搜索量最高的结果,因此我不妨告诉新手如何提出此错误。 我试图通过压缩文件并将其下载(流式传输)到客户端来使用this答案。我在实际的控制器操作结束时返回了return Ok()。我需要退回return new EmptyResult()

答案 3 :(得分:0)

就我而言,我试图打印到控制台上以调试某些内容,并从互联网上的某个地方找到Response.WriteAsync();。我将其粘贴到方法的顶部,这就是为什么我遇到错误"System.InvalidOperationException: StatusCode cannot be set because the response has already started."

的原因

删除Response.WriteAsync("test")方法解决了我的问题!最简单的事情__(ツ)_ //

感谢CularBytes对return new EmptyResult()的建议,该建议打印了我的WriteAsync("test")并暴露了我的错误。

答案 4 :(得分:0)

我的自定义中间件抛出了此错误,但是您可以通过检查它来检查“响应是否已经开始”:

    if (!context.Response.HasStarted)
        { ... }

完整代码:

    private Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        if (!context.Response.HasStarted)
        {
            string result;

            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            result = JsonConvert.SerializeObject(new { error = "An error has occured" });
            _logger.LogError(ex, CreateErrorMessage(context));              

            context.Response.ContentType = "application/json";
            return context.Response.WriteAsync(result);
        }
        else
        {
            return context.Response.WriteAsync(string.Empty);
        }
    }

答案 5 :(得分:0)

解决方案非常简单。一旦编写了响应,就不应将上下文传递给请求委托。如下例所示。

Select col/power(10,(length(trunc(col))-1))
  From your_table
 Where col >= 1000

确保RequestDelegate不会在响应状态中获取上下文。

注意:

设置状态代码后,上下文立即更改为“响应”。