2014-06-05 76 views
13

我們有一個較舊的ASP.NET WebForms應用程序,它通過在客戶端使用jQuery $.ajax()調用來執行AJAX請求,並在用[WebMethod]屬性裝飾的頁面代碼隱藏中調用靜態方法。從調用ASP.NET WebMethod格式錯誤的Json中捕獲錯誤

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

我使用招編輯有效載荷到畸形的JSON嘗試了測試:

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

,並得到了以下ArgumentExceptionJavaScriptObjectDeserializer發送給客戶端的錯誤響應(這是在一個簡單的測試應用程序本地運行,沒有自定義錯誤):

{"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。

有沒有類似的方式來附加自定義處理程序在我的情況?

回答

12

根據該參考源,內部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

邁克爾,這個建議絕對完美,它是一顆寶石!我非常抱歉,你的回答遲到了,我要把它獎給原來的賞金,我已經創造了一個新獎給你。顯然,我必須等待24小時才能獲得獎勵。 – Carson63000

+1

@ Carson63000:我很高興我的代碼適合你。慷慨獎勵新的獎勵,但考慮到它的大小,我會等待看看其他人能否提供更好的答案。 –

1

當你說你在頁面代碼隱藏方式上有標記爲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,似乎現在正遇到一些故障。

UPDATE

讀這post,似乎可以解釋你所面臨的問題:

(...)如果請求是一個實現的System.Web.UI類。頁面,它是一個休息方法調用,WebServiceData類(在前一篇文章中解釋過)用於從頁面調用請求的方法。 該方法被調用後,將調用CompleteRequest方法,繞過所有管道事件並執行EndRequest方法。這使MS AJAX能夠調用頁面上的方法,而不必創建Web服務來調用方法。 (...)

嘗試使用ASP.NET的JavaScript代理,以檢查是否可以捕捉使用Microsoft生成的代碼錯誤。

1

This文章認爲,有兩種方式來擴展它的WebMethods的的SoapExtension是更容易。 This other one展示了一個如何編寫SoapExtension的例子。它看起來像你可以做消息驗證的地方。

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