关于Ajax:使用格式错误的JSON调用ASP.NET WebMethod时捕获错误

Catching errors from calling ASP.NET WebMethod with malformed Json

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

如果webmethod中发生未处理的异常,它不会激发Application_Error事件,因此不会被错误记录器(elmah)捕获。这是众所周知的,而且不是一个问题——我们将所有WebMethod代码都包装在Try-Catch块中,但将异常手动记录到Elmah。

然而,有一件事让我很为难。如果将格式不正确的JSON发布到webmethod url,它会在输入代码之前引发异常,我找不到任何方法来捕获它。

例如,此WebMethod签名

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

通常使用json负载调用,比如:

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

我尝试使用fiddler将有效负载编辑到格式错误的json:

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

并从JavaScriptObjectDeserializer收到以下ArgumentException错误响应,发送给客户端(这是在本地运行的简单测试应用程序中,没有自定义错误):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{"Message":"Unterminated string passed in. (32): {"stringParam":"oh hai","intPara","StackTrace":"   at
System.Web.Script.Serialization.JavaScriptObjectDeserializer.DeserializeString()

   at
System.Web.Script.Serialization.JavaScriptObjectDeserializer.DeserializeMemberName()

   at
System.Web.Script.Serialization.JavaScriptObjectDeserializer.DeserializeDictionary(Int32 depth)

   at
System.Web.Script.Serialization.JavaScriptObjectDeserializer.DeserializeInternal(Int32 depth)

   at
System.Web.Script.Serialization.JavaScriptObjectDeserializer.BasicDeserialize(String input, Int32 depthLimit, JavaScriptSerializer serializer)

   at
System.Web.Script.Serialization.JavaScriptSerializer.Deserialize(JavaScriptSerializer serializer, String input, Type type, Int32 depthLimit)

   at
System.Web.Script.Serialization.JavaScriptSerializer.Deserialize[T](String input)

   at
System.Web.Script.Services.RestHandler.GetRawParamsFromPostRequest(HttpContext context, JavaScriptSerializer serializer)

   at
System.Web.Script.Services.RestHandler.GetRawParams(WebServiceMethodData methodData, HttpContext context)

   at
System.Web.Script.Services.RestHandler.ExecuteWebServiceCall(HttpContext context, WebServiceMethodData methodData)","ExceptionType":"System.ArgumentException"}

它仍然没有触发Application_Error事件,而且它从未输入我们的代码,因此我们不能自己记录错误。

我发现了一个类似的问题,其中有一个指向博客文章"如何为Web服务创建全局异常处理程序"的指针,但它似乎只对SOAP Web服务有效,而不是Ajax获取/发布。

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


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

1
2
3
4
5
6
7
8
9
10
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);
    }
}

我唯一能想到的解决方法是创建一个输出过滤器来截取和记录输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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中安装过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
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);
        }
    }
}


本文建议有两种方法来扩展WebMethods,其中soapextension更容易实现。另一个示例演示如何编写SoapExtension。它看起来像是您可以进行消息验证的地方。


当您说页面代码后面有标记为WebMethod的静态方法,并且您说您使用$.ajax时,这听起来是错误的。但我将给予怀疑的好处,因为我不知道你们系统的特殊性。

无论如何,请测试:

  • 页面上应该有一个这样的脚本管理器:(**1)

  • 然后在您有$.ajax调用的地方,像这样调用page方法:(**2)

(** 1)

1
2
3
4
5
6
<asp:ScriptManager ID="smPageManager"
        runat="server"
        EnablePageMethods="true"
        ScriptMode="Release"
        LoadScriptsBeforeUI="true">
</asp:ScriptManager>

(** 2)

1
2
3
4
5
PageMethods.LeWebMethod("hero", 1024, function(response){
    alert(response);
}, function(error){
    alert(error);
});

了解使用ASP.NET Ajax库的正确方法,对其进行测试,并查看错误是否正确地报告给您。

P.S:对不起,书签样式的符号,但是,现在似乎出现了一些故障。

更新

阅读这篇文章,似乎可以解释你面临的问题:

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

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


这里有一个用我自己的版本替换内部resthandler实现的解决方案。您可以将异常记录在WriteExceptionJSonString方法中。这将使用动态替换C方法内容时提供的答案?换掉方法。我已经确认,如果在global.asax应用程序的start方法中添加一个对replaceRestHandler的调用,它将对我起作用。没有运行很长时间或在生产中,所以使用风险自理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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;
                }
            }
        }
    }
}

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

栈溢出

非封闭进化

ASP.NET

痴迷

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