SignalR Core 1.0 периодически меняет регистр метода HTTP для POST без сигналаR, необходимо исправить (AKA Random 404 Errors) - PullRequest
0 голосов
/ 03 июня 2018

Я всегда неохотно заявляю, что ошибка, которую я вижу, на самом деле является ошибкой .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          
        });
    }

Вызов этого кода с помощью вызова jquery ajax или Postman работает каждый раз безупречно, если у меня нет каких-либо концентраторов SignalR Core 1.0, зарегистрированных в методе startup.cs.Однако, когда я регистрирую следующее в файле 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-методом, он преобразует его в пустой метод,А поскольку метод My action требует HTTP POST, возвращается код состояния 404.Многие HTTP POSTS для этой конечной точки работают нормально, но часто возникает проблема, которую я только что описал.

Чтобы убедиться, что мой клиентский код не является частью проблемы, я смог воспроизвести мою проблему, используя Postman для выполнения запросов.Чтобы убедиться, что POST действительно отправляется, а не Post, я использовал Fiddler, чтобы посмотреть, что происходит по проводам.Все это документировано ниже.

Вот первый запрос (который всегда работает), выполняемый почтальоном:

enter image description here

Вотвторой (идентичный!) запрос, выполненный через Postman, этот запрос привел к 404:

enter image description here

Вот каков первый запрос (тот, который работал правильно) в фиддлере выглядело так:

enter image description here

Вот как выглядел второй запрос в фиддлере:

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 Method.Здесь вещи становятся действительно интересными.Если я просто наведу указатель мыши на свойство в отладчике, оно тоже будет пустым.

enter image description here

Но, если я разверну объект requestFeature и посмотрю на свойство Methodтам, это сообщение !!!

enter image description here

Это само по себе кажется сумасшествием.Как два вида свойства SAME в отладчике могут иметь разные значения ???!Казалось бы, какой-то код преобразовал POST в Post, и на каком-то уровне система знает, что Post не является допустимым http-методом, поэтому в некоторых представлениях этой переменной он преобразуется в пустую строку.Но это так странно!

Кроме того, через Postman и Fiddler мы ясно видели, что POST был отправлен, так как же он изменился на Post?Какой код это сделал?Я хотел бы заявить, что это не может быть мой код, так как я проверяю значение RequestFeature, прежде чем любой другой мой код, связанный с запросом, получит шанс на выполнение.Кроме того, если я закомментирую одну строку кода, которая регистрирует этот концентратор SignalR, то POST никогда не преобразуется в Post, и я никогда не получаю 404. Но с этим зарегистрированным концентратором SignalR я периодически получаю такое поведение.

Есть ли какие-либо SignalR или другие .net Core переключатели, которые я могу включить, чтобы получить лучшую информацию о трассировке или регистрации, чтобы увидеть, когда POST меняется на Post?Есть ли способ это исправить?

1 Ответ

0 голосов
/ 27 июня 2018

Этот вопрос был рассмотрен с помощью этой проблемы GitHub https://github.com/aspnet/KestrelHttpServer/issues/2591, которая была первоначально открыта, когда кто-то еще также наблюдал случайные ошибки 404

Я хочу особенно поблагодарить @ ben-adams за его помощь впонимание того, что происходит.

Позвольте мне начать с того, что это не является ошибкой в ​​фреймворке.Это была ошибка в моем коде.Как можно получить то, что я наблюдал?

Ну, это так ...
В некоторых частях HttpRequest метод является строкой, а в других - перечислением.Значение перечисления для POST - Post.Вот почему происходило преобразование случая.

Причина, по которой одна часть запроса говорила Post, в то время как другая часть отображала значение метода пустой строки, была в том, что объект запроса был склеен, потому что я получил доступэто в то время, когда это было между запросами.

Как я это сделал?вы можете задаться вопросомХорошо, позвольте мне сказать вам, потому что сюжет утолщается ...

У меня получается, что у меня есть некоторый код регистрации, который собирает контекстную информацию при вызове, и одна из частей контекстной информации, которую он собирает, является текущим запросом..method.Когда этот код журнала вызывается из основного потока, проблема не возникает.

Однако в моей системе есть некоторый код, который выполняется в фоновых потоках, которые либо запускаются через Timer, либо через ThreadPool.QueueUserWorkItem,Если этот код встречает исключение, он будет вызывать тот же код регистратора.

Когда мой код регистратора, работающий в фоновом потоке, проверяет текущий httpContext через IHttpContextAccessor, я полностью ожидал, что он получит значение NULL.И, конечно, тот же код в той же ситуации при доступе к текущему HttpContext через HttpContext.Current на веб-сайте, отличном от .Net Core, получает нулевое значение.Но, как оказалось, в ядре .Net он не получал ноль, он получал объект.Но этот объект был для запроса, который уже завершился, и чей объект запроса уже был сброшен !!!

Начиная с .Net Core 2.0, HttpContext и его дочерние объекты, такие как request, сбрасываются после закрытия соединения для запроса.Таким образом, объект HttpContext (и его объект запроса), который получал код регистратора при запуске в фоновом потоке, был объектом, который был сброшен.Это request.Path, например, был нулевым.

Оказывается, что запрос в этом состоянии не ожидает, что его свойство request.Method будет доступно.И это объединяет работы для следующего поступающего запроса. В конечном счете, это является причиной того, почему следующий поступивший запрос в итоге возвратил ошибку 404.

Итак, как мы можем это исправить?Почему IHttpContextAccessor возвращает объект, а не ноль, в этой ситуации вне контекста, особенно учитывая, что объект может быть между запросами?Ответ заключается в том, что когда я использовал Timer или ThreadPool.QueueUserWorkItem для создания фоновой задачи, контекст выполнения передавался в новый поток.Это именно то, что происходит по умолчанию при использовании этих методов API.Но внутренне IHttpContextAccessor использует AsyncLocal для отслеживания текущего HttpContext, и, поскольку мой новый поток получил контекст выполнения из основного потока, он имел доступ к тому же AsyncLocal.И поэтому IHttpContextAccessor предоставил объект, а не ноль, который я ожидал при вызове из фонового потока.

Исправление?(Спасибо, @ Бен-Адамс!) Вместо того, чтобы звонить ThreadPool.QueueUserWorkItem, мне нужно было позвонить ThreadPool.UnsafeQueueUserWorkItem.Этот метод НЕ передает текущий контекст выполнения в новый поток, и, следовательно, новый поток не будет иметь доступа к этим AsyncLocals из основного потока.Как только я это сделал, IHttpContextAccessor затем возвращал ноль при вызове из фонового потока вместо того, чтобы возвращать объект, который был между запросами и неприкасаемым.Да!

При создании «Таймера» мне также нужно было изменить свой код так, чтобы он не передавался в контексте выполнения.Вот код, который я использую (который был вдохновлен некоторыми предложенными Бен-Адамсом):

 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 AsyncLocal находился в области видимости.Отсутствие передачи контекста выполнения в фоновые потоки устраняет эту проблему.

...