Подтвердить что ты не робот

Улавливание ошибок при вызове ASP.NET WebMethod с неправильным Json

У нас есть более раннее приложение ASP.NET WebForms, которое выполняет запрос AJAX, используя вызовы jQuery $.ajax() на стороне клиента, вызывая статические методы в коде кода, украшенными атрибутами [WebMethod].

Если в WebMethod возникает необработанное исключение, оно не запускает событие Application_Error и поэтому не подхвачено нашим регистратором ошибок (ELMAH), Это хорошо известно, а не проблема. У нас есть весь код WebMethod, заключенный в блоки try-catch с исключениями, которые вручную регистрируются в ELMAH.

Однако есть один случай, который меня озадачил. Если некорректный Json отправлен на URL WebMethod, он выдает исключение перед входом в наш код, и я не могу найти способ его захватить.

например. эта подпись WebMethod

[WebMethod]
public static string LeWebMethod(string stringParam, int intParam)

Обычно вызывается с полезной нагрузкой Json, например:

{"stringParam":"oh hai","intParam":37}

Я попробовал тест с помощью Fiddler для редактирования полезной нагрузки для искаженного Json:

{"stringParam":"oh hai","intPara

И получил следующий ответ ArgumentException ответ от JavaScriptObjectDeserializer, отправленный клиенту (это в простом тестовом приложении, работающем локально без каких-либо пользовательских ошибок):

{"Message":"Unterminated string passed in. (32): {\"stringParam\":\"oh hai\",\"intPara","StackTrace":"   at
System.Web.Script.Serialization.JavaScriptObjectDeserializer.DeserializeString()\r\n   at
System.Web.Script.Serialization.JavaScriptObjectDeserializer.DeserializeMemberName()\r\n   at
System.Web.Script.Serialization.JavaScriptObjectDeserializer.DeserializeDictionary(Int32 depth)\r\n   at 
System.Web.Script.Serialization.JavaScriptObjectDeserializer.DeserializeInternal(Int32 depth)\r\n   at 
System.Web.Script.Serialization.JavaScriptObjectDeserializer.BasicDeserialize(String input, Int32 depthLimit, JavaScriptSerializer serializer)\r\n   at 
System.Web.Script.Serialization.JavaScriptSerializer.Deserialize(JavaScriptSerializer serializer, String input, Type type, Int32 depthLimit)\r\n   at 
System.Web.Script.Serialization.JavaScriptSerializer.Deserialize[T](String input)\r\n   at 
System.Web.Script.Services.RestHandler.GetRawParamsFromPostRequest(HttpContext context, JavaScriptSerializer serializer)\r\n   at 
System.Web.Script.Services.RestHandler.GetRawParams(WebServiceMethodData methodData, HttpContext context)\r\n   at 
System.Web.Script.Services.RestHandler.ExecuteWebServiceCall(HttpContext context, WebServiceMethodData methodData)","ExceptionType":"System.ArgumentException"}

Он все еще не запускает событие Application_Error, и он никогда не входит в наш код, поэтому мы не можем самостоятельно регистрировать ошибку.

Я нашел аналогичный вопрос, который получил указатель на сообщение в блоге " "Как создать глобальный обработчик исключений для веб-службы" , но это кажется действительным только для веб-сервисов SOAP, а не AJAX GET/POST.

Есть ли аналогичный способ привязки пользовательского обработчика в моей ситуации?

4b9b3361

Ответ 1

В соответствии с исходным источником внутренний метод RestHandler.ExecuteWebServiceCall захватывает все исключения, создаваемые GetRawParams, и просто записывает их в поток ответов, поэтому Application_Error не вызывается:

internal static void ExecuteWebServiceCall(HttpContext context, WebServiceMethodData methodData) {
    try {
        ...
        IDictionary<string, object> rawParams = GetRawParams(methodData, context);
        InvokeMethod(context, methodData, rawParams);
    }
    catch (Exception ex) {
        WriteExceptionJsonString(context, ex);
    }
}

Единственным обходным решением, которое я могу придумать, является создание выходного фильтра, который перехватывает и регистрирует вывод:

public class PageMethodExceptionLogger : Stream
{
    private readonly HttpResponse _response;
    private readonly Stream _baseStream;
    private readonly MemoryStream _capturedStream = new MemoryStream();

    public PageMethodExceptionLogger(HttpResponse response)
    {
        _response = response;
        _baseStream = response.Filter;
    }

    public override void Close()
    {
        if (_response.StatusCode == 500 && _response.Headers["jsonerror"] == "true")
        {
            _capturedStream.Position = 0;
            string responseJson = new StreamReader(_capturedStream).ReadToEnd();
            // TODO: Do the actual logging.
        }

        _baseStream.Close();
        base.Close();
    }

    public override void Flush()
    {
        _baseStream.Flush();
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        return _baseStream.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
        _baseStream.SetLength(value);
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return _baseStream.Read(buffer, offset, count);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        _baseStream.Write(buffer, offset, count);
        _capturedStream.Write(buffer, offset, count);
    }

    public override bool CanRead { get { return _baseStream.CanRead; } }
    public override bool CanSeek { get { return _baseStream.CanSeek; } }
    public override bool CanWrite { get { return _baseStream.CanWrite; } }
    public override long Length { get { return _baseStream.Length; } }

    public override long Position
    {
        get { return _baseStream.Position; }
        set { _baseStream.Position = value; }
    }
}

В Global.asax.cs(или в модуле HTTP) установите фильтр в Application_PostMapRequestHandler:

protected void Application_PostMapRequestHandler(object sender, EventArgs e)
{
    HttpContext context = HttpContext.Current;
    if (context.Handler is Page && !string.IsNullOrEmpty(context.Request.PathInfo))
    {
        string contentType = context.Request.ContentType.Split(';')[0];
        if (contentType.Equals("application/json", StringComparison.OrdinalIgnoreCase))
        {
            context.Response.Filter = new PageMethodExceptionLogger(context.Response);
        }
    }
}

Ответ 2

Когда вы говорите, что у вас есть статические методы для кода страницы, помеченного WebMethod, и вы говорите, что используете $.ajax, это звучит просто неправильно. Но я дам пользу сомнению, так как я не знаю особенностей вашей системы.

В любом случае, пожалуйста, проверьте это:

  • У вас должен быть ScriptManager на вашей странице, похожий на это: (** 1)

  • Затем в том месте, где у вас есть ваш вызов $.ajax, вызовите метод страницы следующим образом: (** 2)

(** 1)

<asp:ScriptManager ID="smPageManager"
        runat="server"
        EnablePageMethods="true" 
        ScriptMode="Release" 
        LoadScriptsBeforeUI="true"> 
</asp:ScriptManager>

(** 2)

PageMethods.LeWebMethod("hero", 1024, function(response){
    alert(response);
}, function(error){
    alert(error);
});

Знайте, используя ASP.NET Ajax Library надлежащим образом, дайте ему тест и посмотрите, правильно ли возвращается сообщение об ошибке.

P.S: Извините за нотацию стиля закладки, но SO, похоже, сейчас испытывает некоторые сбои.

UPDATE

Чтение этой почты, похоже, объясняет проблему, с которой вы сталкиваетесь:

(...) Если запрос предназначен для класса, реализующего System.Web.UI.Page, и это вызов метода rest, класс WebServiceData (который был объяснен в предыдущем сообщении ) используется для вызова запрошенного метода со страницы. После вызова метода вызывается метод CompleteRequest, минуя все события конвейера и выполняя метод EndRequest. Это позволяет MS AJAX иметь возможность вызывать метод на странице вместо того, чтобы создавать веб-службу для вызова метода. (...)

Попробуйте использовать ASP.NET JavaScript Proxies, чтобы проверить, можете ли вы захватить ошибку с помощью Microsoft Generated Code.

Ответ 4

Эти ссылки могут помочь вам справиться с ошибкой на стороне клиента,

fooobar.com/questions/13028/...

unseenrevolution

asp.net

encosia

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

Ответ 5

Вот решение, которое заменяет внутреннюю реализацию RestHandler моей собственной версией. Вы можете зарегистрировать исключение в методе WriteExceptionJsonString. Это использует ответ, указанный в Динамически заменить содержимое метода С#?, чтобы заменить метод. Я подтвердил, что это работает для меня, если я добавлю вызов ReplaceRestHandler в свой метод Global.asax Application_Start. Не запускайте это очень долго или в производстве, поэтому используйте на свой страх и риск.

using System;
using System.Collections.Specialized;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Web;
using Newtonsoft.Json;

namespace Royal.Common.WebStuff
{
    public static class RestHandlerUtils
    {
        internal static void WriteExceptionJsonString(HttpContext context, Exception ex, int statusCode)
        {
            string charset = context.Response.Charset;
            context.Response.ClearHeaders();
            context.Response.ClearContent();
            context.Response.Clear();
            context.Response.StatusCode = statusCode;
            context.Response.StatusDescription = HttpWorkerRequest.GetStatusDescription(statusCode);
            context.Response.ContentType = "application/json";
            context.Response.AddHeader("jsonerror", "true");
            context.Response.Charset = charset;
            context.Response.TrySkipIisCustomErrors = true;
            using (StreamWriter streamWriter = new StreamWriter(context.Response.OutputStream, new UTF8Encoding(false)))
            {
                if (ex is TargetInvocationException)
                    ex = ex.InnerException;
                var error = new OrderedDictionary();
                error["Message"] = ex.Message;
                error["StackTrace"] = ex.StackTrace;
                error["ExceptionType"] = ex.GetType().FullName;
                streamWriter.Write(JsonConvert.SerializeObject(error));
                streamWriter.Flush();
            }
        }

        public static void ReplaceRestHandler()
        {
            ///questions/40927/dynamically-replace-the-contents-of-a-c-method
            var methodToInject = typeof(RestHandlerUtils).GetMethod("WriteExceptionJsonString",
                BindingFlags.NonPublic | BindingFlags.Static);
            var asm = typeof(System.Web.Script.Services.ScriptMethodAttribute).Assembly;
            var rhtype = asm.GetType("System.Web.Script.Services.RestHandler");
            var methodToReplace = rhtype
                .GetMethod("WriteExceptionJsonString", BindingFlags.NonPublic | BindingFlags.Static, null,
                    new Type[] {typeof(HttpContext), typeof(Exception), typeof(int)}, null);

            RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
            RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);

            unsafe
            {
                if (IntPtr.Size == 4)
                {
                    int* inj = (int*) methodToInject.MethodHandle.Value.ToPointer() + 2;
                    int* tar = (int*) methodToReplace.MethodHandle.Value.ToPointer() + 2;
                    *tar = *inj;
                }
                else
                {
                    long* inj = (long*) methodToInject.MethodHandle.Value.ToPointer() + 1;
                    long* tar = (long*) methodToReplace.MethodHandle.Value.ToPointer() + 1;
                    *tar = *inj;
                }
            }
        }
    }
}

Ответ 6

@MichaelLiu ответ отличный, но ломается в классическом режиме (работает в интегрированном режиме). Это из-за _response.Headers["jsonerror"], который не поддерживается в классическом режиме. Я оставил этот флажок и все еще, кажется, работает нормально для меня, так как все состояния 501 все равно должны быть ошибками. Не могу придумать сценарий, где нужна дополнительная проверка.