SignalR Core 1.0间歇性地改变非信号R POST的http方法的情况,需要修复(AKA Random 404 Errors)

时间:2018-06-02 23:04:46

标签: asp.net-core-2.0 asp.net-core-signalr

我总是不愿意声称我看到的一个错误实际上是一个.Net Core错误,但在花了8个多小时调查以下错误后,它看起来像是一个.Net Core SignalR错误。我需要一些技术来进一步跟踪它并进行修复。

对错误进行修改的第一条规则是尝试创建可以一致地重现错误的最少量的代码。虽然我无法在项目的小展台上重现它,但我努力尝试将正在发生的事情归零。

我有一个控制器,其中包含以下操作方法

    [HttpPost]
    [Route("/hack/ajax/start")]
    public JsonResult AjaxStart([FromBody] JObject data) {

        //A call to some method that does some work

        return Json(new {
            started = true          
        });
    }

如果我没有在startup.cs方法中注册任何SignalR Core 1.0集线器,则每次通过jquery ajax调用或Postman调用此代码都可以正常运行。但是,当我在startup.cs文件中注册以下内容时,我会遇到间歇性问题。

 namespace App.Site.Home {
     public class HackHub : Hub {
         public async Task SendMessage(string status, string progress) {
             await Clients.All.SendAsync("serverMsg", status, progress);
         }
     }
 }

Startup.cs ConfigureServices包含

 services.AddSignalR();

Startup.cs Configure包含

       app.UseSignalR(routes => {
            routes.MapHub<App.Site.Home.HackHub>("/hub/hack");
        });

如果我要评论routes.MapHub<App.Site.Home.HackHub>("/hub/hack");以上的一行,那么一切都很好。然而,当这条线存在时,(即某些SignalR集线器已经注册)那就是当我开始有趣的时候,即使我没有在使用集线器的客户端或服务器上执行代码!

问题在于,有时当针对上面的操作方法发出HTTP POST请求时,.Net Core(SignalR ??)中的某些东西正在将POST方法转换为Post,然后因为Post不是有效的HTTP方法将其转换为空白方法。由于我的操作方法需要HTTP POST,因此返回404状态代码。该端点的许多HTTP POSTS工作正常,但我刚刚描述的问题经常出现。

为了确保我的客户端代码不是问题的一部分,我能够使用Postman重现我的问题来发出请求。为了确保POST实际上是发送而不是Post,我使用Fiddler来观察线路上的内容。所有这些都记录在下面。

这是第一个通过Postman完成的请求(总是有效):

enter image description here

这是通过Postman完成的第二个(相同的!)请求,这个请求产生了404:

enter image description here

这是第一个请求(正常工作的请求)看起来像fiddler:

enter image description here

这是fiddler中第二个请求的样子:

enter image description here

如您所见,请求完全相同。但肯定不是。

因此,为了更好地了解服务器所看到的内容,我将以下代码添加到startup.cs Configure方法的开头。由于它的位置,对于请求,此代码可以在任何其他应用程序代码或中间件之前运行。

 public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
        //for debugging
        app.Use(async (context, next) => {
            if(context.Request.Method == "") {
                string method = context.Request.Method;
                string path = context.Request.Path;

                IHttpRequestFeature requestFeature = context.Features.Get<IHttpRequestFeature>();
                string kestralHttpMethod = requestFeature.Method;
                string stop = path;
            }
            await next();
        });

       //more code here...
}

对于第一个请求,request.Method是人们期望的POST:

enter image description here

但是对于第二个请求请求。方法是空白的!!

enter image description here

为了进一步研究这个问题,我访问了requestFeature并检查了那里的Http方法。这是事情变得非常有趣的地方。如果我只是将鼠标悬停在debuggger中的属性上,它也是空白的。

enter image description here

但是,如果我展开requestFeature对象并查看那里的Method属性,那么它是Post !!!

enter image description here

仅此一点似乎很疯狂。如何在调试器中的SAME属性的两个视图具有不同的值???!似乎有些代码将POST转换为Post,并且在某种程度上系统知道Post不是有效的http方法,所以在该变量的某些视图中它被转换为空字符串。但那太奇怪了!

另外,我们清楚地看到通过Postman和Fiddler发送了POST,那么它是如何被改为Post的呢?那是什么代码?我想声称它不能是我的代码,因为在检查与请求相关的任何其他代码有机会运行之前,我正在检查RequestFeature的值。另外,如果我注释掉注册那个SignalR集线器的一行代码,那么POST永远不会转换为Post,我永远不会得到404.但是随着SignalR集线器的注册,我会定期得到这种行为。

是否有任何SignalR或其他.net核心交换机我可以打开以获得更好的跟踪或日志信息,以查看POST何时更改为Post?有没有办法来解决这个问题?

1 个答案:

答案 0 :(得分:1)

此问题是通过GitHub问题https://github.com/aspnet/KestrelHttpServer/issues/2591来调查的,该问题最初是在其他人也观察到随机404错误时提出的

我要特别感谢@ ben-adams对理解正在发生的事情的帮助。

让我首先说这并不是框架中的错误。这是我代码中的错误。怎么给我我所观察到的?

好吧,就像这样...
在HttpRequest的某些部分中,该方法是一个字符串,但在其他部分中,它是一个枚举。 POST的枚举值为Post。这就是为什么要进行大小写转换的原因。

请求的一部分说出Post而另一部分显示空白字符串的Method值的原因是因为请求对象被粘住了,因为我在两次请求之间都访问了它。 / p>

我该怎么做?你可能想知道。好吧,我告诉你,因为情节变浓了...

我发现我有一些日志记录代码,这些代码在调用时会收集上下文信息,而收集的上下文信息之一就是当前的request.Method。从主线程调用此日志记录代码时,没有问题。

但是,我的系统中确实有一些代码在通过TimerThreadPool.QueueUserWorkItem启动的后台线程上运行。如果此代码遇到异常,它将调用相同的记录器代码。

当我的记录器代码在后台线程上运行时,通过IHttpContextAccessor检查当前的httpContext,我完全希望它接收到null。当然,在非.Net Core网站中通过HttpContext.Current访问当前HttpContext时,在相同情况下的同一代码也不会收到null。但事实证明,在.Net核心下,它没有收到null,而是在接收对象。但是该对象用于已完成的请求,并且谁的请求对象已被重置!!!

从.Net Core 2.0开始,HttpContext及其子对象(如request)在请求连接关闭后重置。因此,在后台线程上运行时,记录器代码获得的HttpContext对象(及其请求对象)是已重置的对象。是request.Path例如为null。

事实证明,处于此状态的请求不希望其为request.Method属性。这样做会增加下一个请求的工作量。最终,这就是为什么下一个请求最终返回404错误的原因。

那么我们该如何解决呢?为什么在这种脱离上下文的情况下,IHttpContextAccessor返回一个对象而不是null,特别是考虑到该对象很可能在请求之间?答案是,当我使用Timer或ThreadPool.QueueUserWorkItem创建后台任务时,执行上下文正在流向新线程。这就是使用这些API方法时默认情况下发生的情况。但是,在内部IHttpContextAccessor使用AsyncLocal来跟踪当前的HttpContext,并且由于我的新线程从主线程接收到了执行上下文,因此它可以访问相同的AsyncLocal。因此IHttpContextAccessor提供了一个对象,而不是从后台线程调用时所期望的null。

解决办法? (谢谢@ Ben-Adams!)我没有打电话给ThreadPool.QueueUserWorkItem,而是打电话给了ThreadPool.UnsafeQueueUserWorkItem。此方法不会将当前的执行上下文传递给新线程,因此新线程将无法从主线程访问那些AsyncLocals。完成此操作后,IHttpContextAccessor然后从后台线程调用时返回null,而不是返回介于请求之间且不可触摸的对象。是的!

创建“计时器”时,我还需要更改代码以使其不影响执行上下文。这是我使用的代码(它受某些@ Ben-Adams的启发):

 public static Timer GetNewTimer(TimerCallback callback, object state, int dueTime, int interval) {

        bool didSuppress = false;
        try {
            if (!ExecutionContext.IsFlowSuppressed()) {
                //We need to suppress the flow of the execution context so that it does not flow to our
                //new asynchronous thread. This is important so that AsyncLocals (like the one used by 
                //IHttpaccessor) do not flow to the new thread we are pushing our work to.  By not flowing the
                //execution context, IHttpAccessor wil return null rather than bogusly returning a context for 
                //a request that is in between requests.
                //Related info: https://github.com/aspnet/KestrelHttpServer/issues/2591#issuecomment-399978206
                //Info on Execution Context: https://blogs.msdn.microsoft.com/pfxteam/2012/06/15/executioncontext-vs-synchronizationcontext/
                ExecutionContext.SuppressFlow();

                didSuppress = true;
            }

            return new Timer(callback, state, dueTime, interval);

        } finally {
            // Restore the current ExecutionContext
            if (didSuppress) {
                ExecutionContext.RestoreFlow();
            }
        }
    }

这仅剩下一个未解决的问题。我最初的问题是,注册SignalR集线器会使系统表现出这种随机的404行为,但是当没有SignalR集线器被注册时(或我认为),系统就不会表现出这种行为。怎么会这样我真的不知道也许这给系统的某些部分带来了更多的资源压力,从而使问题更容易出现。不确定。我所知道的是,根本的问题是我在没有意识到的情况下将执行上下文传递到我的后台线程,这导致IHttpContextAccessor的{​​{1}}处于范围内。不将执行上下文传递到后台线程即可解决此问题。