通过使用格式错误的Json调用ASP.NET WebMethod来捕获错误

时间:2014-06-05 07:51:12

标签: asp.net ajax error-handling webmethod

我们有一个较旧的ASP.NET WebForms应用程序,它通过在客户端使用jQuery $.ajax()调用来执行AJAX请求,在使用[WebMethod]属性修饰的页面代码隐藏中调用静态方法。

如果WebMethod中发生未处理的异常,则它不会触发Application_Error事件,因此我们的错误记录器(ELMAH)不会将其捕获。这是众所周知的而不是问题 - 我们将所有WebMethod代码包装在try-catch块中,并将异常手动记录到ELMAH。

然而,有一个案例令我难过。如果格式错误的Json发布到WebMethod URL,它会在输入我们的代码之前抛出异常,我找不到任何方法来捕获它。

e.g。这个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事件,它从不输入我们的代码,因此我们无法自行记录错误。

我发现了一个类似的问题,它指向博客文章" How to create a global exception handler for a Web Service"但这似乎只适用于SOAP Web服务,而不适用于AJAX GET / POST。

在我的情况下是否有类似的方法来附加自定义处理程序?

6 个答案:

答案 0 :(得分:17)

根据参考源,内部RestHandler.ExecuteWebServiceCall方法捕获GetRawParams抛出的所有异常,并将它们简单地写入响应流,这就是为什么不调用Application_Error:< / p>

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);
        }
    }
}

答案 1 :(得分:1)

如果您说在代码隐藏的页面上有静态方法标记为WebMethod并且您说您使用$.ajax,那听起来就错了。但是我会从怀疑中获益,因为我不知道你系统的特殊性。

无论如何,请测试一下:

  • 您的页面上应该有一个ScriptManager,如下所示:(** 1)

  • 然后在您进行$.ajax来电的地方,请按照以下方式打电话给您:Page **

<强>(** 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:对于书签样式表示法很抱歉,但是现在似乎遇到了一些故障。

<强>更新

阅读此post,似乎可以解释您所面临的问题:

(...)如果请求是针对实现System.Web.UI.Page的类而且是一个rest方法调用,那么WebServiceData类(在上一篇文章中有解释) )用于从页面调用所请求的方法。 调用方法后,将调用CompleteRequest方法,绕过所有管道事件并执行EndRequest方法。这允许MS AJAX能够在页面上调用方法,而不必创建调用方法的Web服务。 (...)

尝试使用ASP.NET JavaScript代理,检查是否可以使用Microsoft生成的代码捕获错误。

答案 2 :(得分:1)

This文章表明,有两种方法可以扩展WebMethods,SoapExtension更容易。 This other one显示了如何编写SoapExtension的示例。它看起来像是您可以进行消息验证的地方。

答案 3 :(得分:0)

这些链接可能会帮助您在客户端处理错误,

stackoverflow

unseenrevolution

asp.net

encosia

然后你可以从客户端触发一个控制事件,通过服务器传递错误并进行记录。

答案 4 :(得分:0)

这是一个用我自己的版本替换内部RestHandler实现的解决方案。您可以在WriteExceptionJsonString方法中记录异常。这使用Dynamically replace the contents of a C# method?上提供的答案来交换方法。我已经确认,如果我在Global.asax Application_Start方法中添加对ReplaceRestHandler的调用,它对我有用。没有这么长时间或在生产中运行,所以使用风险自负。

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()
        {
            //https://stackoverflow.com/questions/7299097/dynamically-replace-the-contents-of-a-c-sharp-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;
                }
            }
        }
    }
}

答案 5 :(得分:0)

@MichaelLiu的回答很好,但在经典模式下无法使用(在集成模式下工作)。这是因为经典模式不支持_response.Headers["jsonerror"]。我离开了该检查,但对我来说似乎仍然可以正常工作,因为所有状态501都应该是错误。想不到需要额外检查的情况。