调用 ASP.NET WebMethod 时捕获 JSON 格式错误

18
我们有一个旧的ASP.NET WebForms应用程序,通过在客户端使用jQuery $.ajax()调用调用带有[WebMethod]属性的页面代码后端中的静态方法来执行AJAX请求。
如果WebMethod内发生未处理的异常,则不会触发Application_Error事件,因此无法被错误记录器(ELMAH)捕获。这是众所周知的,并不是问题——我们将所有WebMethod代码都包装在try-catch块中,并手动将异常记录到ELMAH。
然而,有一种情况让我感到困惑。如果将格式不正确的Json发布到WebMethod URL,则会在进入我们的代码之前抛出异常,我找不到任何方法来捕获它。
例如,此WebMethod签名:
[WebMethod]
public static string LeWebMethod(string stringParam, int intParam)

通常使用类似于Json有效负载的方式调用:
{"stringParam":"oh hai","intParam":37}

我使用 Fiddler 进行测试,尝试编辑载荷到不完整的 JSON。

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

我收到了以下来自 JavaScriptObjectDeserializerArgumentException 错误响应,它被发送给客户端(这是在没有自定义错误的情况下在本地运行的简单测试应用程序):

{"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事件,也从未进入我们的代码,因此我们无法自己记录错误。 我找到了一个类似的问题,其中提到了博客文章 "如何为Web服务创建全局异常处理程序",但这似乎只适用于SOAP Web服务,而不是AJAX GET / POST。 在我的情况下是否有类似的方法可以附加自定义处理程序?
6个回答

25
根据参考来源,内部的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);
        }
    }
}

1
那看起来是一个非常有前途的想法,我一定会试着沿着这个方向进行实验! - Carson63000
1
Michael,你的建议非常完美,真是太棒了!很抱歉你的答案来得太晚了,无法让你获得原始赏金,我已经创建了一个新的赏金来奖励你。不过,显然我需要等待24小时才能颁发它。 - Carson63000
1
@Carson63000:很高兴我的代码对你有用。你愿意授予一个新的悬赏非常慷慨,但考虑到它的规模,我建议等待看看是否有其他人可以贡献更好的答案。 - Michael Liu
2
伙计,没有人会提供比分析框架参考源代码并为解决方案提供完整代码更好的答案。实际上,我所需要做的就是在你的“TODO:”行中连接我们的log4net记录器。 :-) - Carson63000
1
@DanCsharpster:不行。正如您可以从ExecuteWebServiceCall源代码中看到的那样,抛出的异常被写入响应流作为字符串(您可以使用我的代码中的responseJson变量进行读取),并且没有钩子来访问实际的异常对象。 - Michael Liu
显示剩余9条评论

1

这篇文章建议扩展WebMethods有两种方法,其中SoapExtension是更容易的。另一篇文章提供了一个编写SoapExtension示例的实例。它看起来像是可以进行消息验证的地方。


1
当你说在页面代码后台上标记了WebMethod的静态方法,并且使用$.ajax,这听起来不太对。但我会给予怀疑的好处,因为我不知道你系统的细节。

无论如何,请测试以下内容:

  • 您应该在页面上拥有一个ScriptManager,看起来像这样:(**1)

  • 然后在您调用$.ajax的地方,像这样调用您的Page Method: (**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库,进行测试,并查看是否能够正确地将错误报告回传给您。
注:很抱歉现在的SO好像出现了一些故障,书签样式的标记可能会有问题。
更新:
阅读此 post,似乎可以解释您所面临的问题:
(...)如果请求是针对实现System.Web.UI.Page的类并且是rest方法调用,则使用之前已经解释过的WebServiceData类来从Page调用所请求的方法。在调用了该方法之后,将调用CompleteRequest方法,绕过所有管道事件并执行EndRequest方法。这允许MS AJAX能够调用页面上的方法,而无需创建Web服务来调用方法。(...)
尝试使用ASP.NET JavaScript代理,检查是否能够使用Microsoft生成的代码捕获错误。

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://dev59.com/N2w05IYBdhLWcg3wVwc4
            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;
                }
            }
        }
    }
}

0

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


-1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接