SignalR Core 1.0 间歇性地更改非 signalR POST 的 http 方法的大小写,需要修复(又名随机 404 错误)

SignalR Core 1.0 intermittently changes the case of http method for non signalR POST, need fix (AKA Random 404 Errors)

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

修复 bug 的第一条规则是尝试创建最少数量的代码来始终如一地重现 bug。虽然我无法在一个小型独立项目中重现它,但我一直在努力尝试将正在发生的事情归零。

我有一个具有以下操作方法的控制器

    [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 完成的第一个请求(始终有效):

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

这是第一个请求(正常工作的请求)在 fiddler 中的样子:

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

如您所见,请求是相同的。但是回复肯定不是。

因此,为了更好地了解服务器看到的内容,我将以下代码添加到 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 正如人们所期望的那样:

但是对于第二个请求 request.Method 是空白的!!

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

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

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

还有,我们通过Postman和Fiddler明明看到发送了POST,怎么变成了Post?那是什么代码做的?我想声明它不可能是我的代码,因为在与请求相关的任何其他代码有机会 运行 之前,我正在检查 RequestFeature 的值。此外,如果我注释掉注册该 SignalR 集线器的一行代码,那么 POST 永远不会转换为 Post 并且我永远不会得到 404。但是随着该 SignalR 集线器的注册,我会定期获得此行为.

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

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

时打开的

我要特别感谢@ben-adams 帮助我理解发生了什么。

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

嗯,是这样的...
在 HttpRequest 的某些部分,该方法是一个字符串,但在其他部分它是一个枚举。 POST 的枚举值为 Post。这就是发生大小写转换的原因。

请求的一部分显示 Post 而另一部分显示空白字符串的方法值的原因是因为请求对象被弄乱了,因为我在它访问时访问了它在请求之间。

我是怎么做到的?你可能想知道。好吧,让我告诉你,因为情节变厚了......

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

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

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

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

事实证明,处于此状态的请求并不期望它的 request.Method 属性 被访问。这样做会影响下一个请求的工作。最终,这就是为什么下一个请求以 return 404 错误告终的原因。

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

修复? (谢谢@Ben-Adams!)我没有调用 ThreadPool.QueueUserWorkItem,而是需要调用 ThreadPool.UnsafeQueueUserWorkItem。此方法不会将当前执行上下文流向新线程,因此新线程无法从主线程访问这些 AsyncLocal。一旦我这样做了,IHttpContextAccessor 然后 return 在从后台线程调用时为 null,而不是 returning 一个介于请求和不可触及之间的对象。是啊!

在创建“计时器”时,我还需要更改我的代码,以一种不会流动执行上下文的方式来执行此操作。这是我使用的代码(灵感来自@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 集线器时(或者我认为),系统不会表现出这种行为。这是为什么?我真的不知道。也许它对系统的某些部分施加了更多的资源压力,从而导致问题更容易出现。不确定。我所知道的是,根本问题是我在没有意识到的情况下将执行上下文流到我的后台线程,这导致 IHttpContextAccessorAsyncLocal 在范围内。不将执行上下文流向后台线程可以解决该问题。