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

Mar*_*tke 7 .net c# asp.net-core

相关:在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
Run Code Online (Sandbox Code Playgroud)

但是当完全意外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);
}
Run Code Online (Sandbox Code Playgroud)

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

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()
Run Code Online (Sandbox Code Playgroud)

Cul*_*tes 31

由于这是谷歌上的顶级搜索结果,我不妨告诉新人我是如何想到这个错误的。我试图通过压缩文件并将它们(流式传输)下载到客户端来使用答案。我return Ok()在实际控制器操作结束时返回。我需要回来return new EmptyResult()

  • EmptyResult 真的帮我省了很多麻烦!我衷心的感谢。 (2认同)

J.P*_*rge 5

只是在这里权衡一下:我从处理 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)
Run Code Online (Sandbox Code Playgroud)

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

[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();
}
Run Code Online (Sandbox Code Playgroud)

正如其他答案所提到的,罪魁祸首是return Ok(). 该语句在socket关闭时执行,但此时HTTP连接早已关闭。

我使用的是 NuGet 包Microsoft.AspNetCore.WebSockets版本 2.1.0。


Hyp*_*ate 5

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

    if (!context.Response.HasStarted)
        { ... }
Run Code Online (Sandbox Code Playgroud)

完整代码:

    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);
        }
    }
Run Code Online (Sandbox Code Playgroud)


Mar*_*tke 3

哦,好吧,我正在进一步调查,在尝试更孤立地重现该案例时,我找到了根本原因。

But first some history: I've seen these errors then and when in production, but never was able to reproduce it. Now I am developing another feature and due to an error in my database structure on my development machine this error happens on every request using a decently joined query. So I thought, hey, that's the moment to resolve this issue... but it ended up here.

However, trying to isolate it more, I made an action just throwing a NotImplementedException in my face. And guess what: it works as expected. HTTP 500, no "StatusCode cannot be set, response has already started".

What's the difference? The difference is, that my other failing controller returns this:

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

while DataSourceLoader is a .net class to support DevExpress' DevExtreme JS Framework. It turns out, that result is object, because it returns either a plain array or a wrapping type that also provides some metadata (e.g. for paging and stuff). In my case it applies some Take and Skip but: does not enumerate the search result but returns an IQueryable<>! So enumerating is not done earlier than during rendering the result to JSON. That's why I see the InvalidOperationException above in this special case, but not when throwing it directly from the controller.

Nevertheless, it shows that my exception handling is not working as expected in all cases. I've read that you can replace the whole response stream to avoid this issue, but this has some downsides. So what would be the right way of handling such a situation? I'd like to have the HTTP 500 with my custom JSON content anyway.