使用格式错误的 Json 调用 ASP.NET WebMethod 时捕获错误

Posted

技术标签:

【中文标题】使用格式错误的 Json 调用 ASP.NET WebMethod 时捕获错误【英文标题】:Catching errors from calling ASP.NET WebMethod with malformed Json 【发布时间】:2014-07-26 02:45:30 【问题描述】:

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

如果 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

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

"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 无效。

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

【问题讨论】:

【参考方案1】:

@MichaelLiu 的回答很棒,但在经典模式下中断(在集成模式下工作)。这是因为 _response.Headers["jsonerror"] 在经典模式下不受支持。我取消了该检查,但对我来说似乎仍然可以正常工作,因为无论如何所有状态 501 都应该是错误。想不出需要额外检查的场景。

【讨论】:

【参考方案2】:

这是一个用我自己的版本替换内部 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://***.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;
                
            
        
    

【讨论】:

【参考方案3】:

当您说您在页面代码隐藏上使用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 库的方法,进行测试,看看错误是否正确反馈给您。

P.S:抱歉,书签样式表示法,但是 SO,现在似乎遇到了一些故障。

更新

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

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

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

【讨论】:

【参考方案4】:

根据参考资料,内部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);
        
    

【讨论】:

这看起来是一个很有前途的想法,我一定会尝试按照这些思路进行实验! 迈克尔这个建议绝对完美,它是一个宝石!很抱歉,您的回答来晚了,我才授予它原始赏金,我已经创建了一个新的赏金来奖励您。不过,显然我必须等待 24 小时才能授予它。 @Carson63000:我很高兴我的代码对你有用。而且您非常慷慨地授予新的赏金,但考虑到它的规模,我会等着看其他人是否可以提供更好的答案。 伴侣没有人会提供比分析框架参考源然后为解决方案提供完整代码的人更好的答案。从字面上看,我需要做的就是将我们的 log4net 记录器连接到您的“TODO:”行。 :-) @DanCsharpster:不。从 ExecuteWebServiceCall 源代码中可以看出,抛出的异常作为字符串写入响应流(您可以在我的代码中使用 responseJson 变量读取) ,并且没有挂钩可以访问实际的 Exception 对象。【参考方案5】:

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

【讨论】:

【参考方案6】:

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

***

unseenrevolution

asp.net

encosia

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

【讨论】:

以上是关于使用格式错误的 Json 调用 ASP.NET WebMethod 时捕获错误的主要内容,如果未能解决你的问题,请参考以下文章

asp.net后台cs中的JSON格式变量在前台Js中调用方法

jQuery Ajax 到 asp.net asmx web 服务抛出请求格式无效:application/json

使用 ajax asp.net 调用 Web 服务

asp.net怎么接收post方式传过来的json格式

如何使用 jQuery 和 ASP.NET MVC 从 AJAX 调用返回错误消息?

在Asp.Net Core中关于appsettings.json的快速简便的读取和设置方式