Улавливание ошибок при вызове ASP.NET WebMethod с неправильным Json
У нас есть более раннее приложение ASP.NET WebForms, которое выполняет запрос AJAX, используя вызовы jQuery $.ajax()
на стороне клиента, вызывая статические методы в коде кода, украшенными атрибутами [WebMethod]
.
Если в WebMethod возникает необработанное исключение, оно не запускает событие Application_Error
и поэтому не подхвачено нашим регистратором ошибок (ELMAH), Это хорошо известно, а не проблема. У нас есть весь код WebMethod, заключенный в блоки try-catch с исключениями, которые вручную регистрируются в ELMAH.
Однако есть один случай, который меня озадачил. Если некорректный Json отправлен на URL WebMethod, он выдает исключение перед входом в наш код, и я не могу найти способ его захватить.
например. эта подпись 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
, и он никогда не входит в наш код, поэтому мы не можем самостоятельно регистрировать ошибку.
Я нашел аналогичный вопрос, который получил указатель на сообщение в блоге " "Как создать глобальный обработчик исключений для веб-службы" , но это кажется действительным только для веб-сервисов SOAP, а не AJAX GET/POST.
Есть ли аналогичный способ привязки пользовательского обработчика в моей ситуации?
Ответы
Ответ 1
В соответствии с исходным источником внутренний метод 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);
}
}
}
Ответ 2
Когда вы говорите, что у вас есть статические методы для кода страницы, помеченного 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 Library надлежащим образом, дайте ему тест и посмотрите, правильно ли возвращается сообщение об ошибке.
P.S: Извините за нотацию стиля закладки, но SO, похоже, сейчас испытывает некоторые сбои.
UPDATE
Чтение этой почты, похоже, объясняет проблему, с которой вы сталкиваетесь:
(...) Если запрос предназначен для класса, реализующего System.Web.UI.Page, и это вызов метода rest, класс WebServiceData (который был объяснен в предыдущем сообщении ) используется для вызова запрошенного метода со страницы. После вызова метода вызывается метод CompleteRequest, минуя все события конвейера и выполняя метод EndRequest. Это позволяет MS AJAX иметь возможность вызывать метод на странице вместо того, чтобы создавать веб-службу для вызова метода. (...)
Попробуйте использовать ASP.NET JavaScript Proxies, чтобы проверить, можете ли вы захватить ошибку с помощью Microsoft Generated Code.
Ответ 3
В этой статье предлагается две возможности расширения WebMethods, из которых SoapExtension проще. Этот другой вариант показывает пример написания SoapExtension. Это похоже на место, где вы можете выполнять проверку сообщений.
Ответ 4
Эти ссылки могут помочь вам справиться с ошибкой на стороне клиента,
fooobar.com/questions/13028/...
unseenrevolution
asp.net
encosia
тогда вы можете вызвать событие управления с клиентской стороны, чтобы передать ошибку через сервер и выполнить регистрацию.
Ответ 5
Вот решение, которое заменяет внутреннюю реализацию RestHandler моей собственной версией. Вы можете зарегистрировать исключение в методе WriteExceptionJsonString. Это использует ответ, указанный в Динамически заменить содержимое метода С#?, чтобы заменить метод. Я подтвердил, что это работает для меня, если я добавлю вызов ReplaceRestHandler в свой метод Global.asax Application_Start. Не запускайте это очень долго или в производстве, поэтому используйте на свой страх и риск.
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()
{
///questions/40927/dynamically-replace-the-contents-of-a-c-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;
}
}
}
}
}
Ответ 6
@MichaelLiu ответ отличный, но ломается в классическом режиме (работает в интегрированном режиме). Это из-за _response.Headers["jsonerror"]
, который не поддерживается в классическом режиме. Я оставил этот флажок и все еще, кажется, работает нормально для меня, так как все состояния 501 все равно должны быть ошибками. Не могу придумать сценарий, где нужна дополнительная проверка.