Есть ли способ уловить все ошибки в веб-службе AJAX?

Я хотел бы уловить любое необработанное исключение, созданное в веб-службе ASP.NET, но ничто из того, что я пробовал, пока не работает.

Во-первых, событие HttpApplication.Error не запускается в веб-службах, поэтому вы можете отключиться.

Следующий подход состоял в том, чтобы реализовать расширение мыла и добавить его в web.config с помощью:

<soapExtensionTypes>
   <add type="Foo" priority="1" group="0" />
</soapExtensionTypes>

Однако это не работает, если вы вызываете веб-метод через JSON (что делает мой веб-сайт исключительно).

Моей следующей идеей было бы написать собственный HttpHandler для .asmx, который, мы надеемся, выйдет из System.Web.Script.Services.ScriptHandlerFactory и сделает что-то умное. Я еще не пробовал.

Есть ли какой-то подход, который мне не хватает? Спасибо!

Mike

UPDATE:

Я кратко изложу возможные варианты:

1) Поднимитесь на WCF, что значительно облегчает эту задачу.

2) Поскольку вы не можете подкласс или переопределить класс RestHandler, вам придется повторно реализовать все это как свой собственный IHttpHandler или использовать отражение, чтобы вручную вызвать его методы. Поскольку источник для RestHandler является общедоступным и составляет всего около 500 строк, что делает вашу собственную версию не большой работой, но тогда вы будете нести ответственность за ее сохранение. Я также не знаю никаких лицензионных ограничений, связанных с этим кодом.

3) Вы можете обернуть свои методы в блоки try/catch или, возможно, использовать выражения LAMBDA, чтобы сделать этот код немного более чистым. Вам все равно потребуется изменить каждый метод в вашей веб-службе.

Ответы

Ответ 1

Как описано в Захват всех необработанных исключений автоматически с помощью WebService, на самом деле нет хорошего решения.

Причина, по которой вы не можете захватить HttpApplication.Error и т.д., связана с тем, как RestHandler был реализован хорошими людьми в Microsoft. В частности, RestHandler явно улавливает (обрабатывает) исключение и записывает детали исключения в Response:

internal static void ExecuteWebServiceCall(HttpContext context, WebServiceMethodData methodData)
{
    try
    {
        NamedPermissionSet namedPermissionSet = HttpRuntime.NamedPermissionSet;
        if (namedPermissionSet != null)
        {
            namedPermissionSet.PermitOnly();
        }
        IDictionary<string, object> rawParams = GetRawParams(methodData, context);
        InvokeMethod(context, methodData, rawParams);
    }
    catch (Exception exception)
    {
        WriteExceptionJsonString(context, exception);
    }
}

Что еще хуже, нет чистой точки расширения (которую я мог найти), где вы можете изменить/расширить поведение. Если вы хотите пойти по пути написания собственного IHttpHandler, я верю, что вам придется переустановить RestHandler (или RestHandlerWithSession); независимо от Reflector будет вашим другом.

Для тех, кто может изменить свои WebMethods

Если вы используете Visual Studio 2008 или более позднюю версию, использование выражений Lambda делает вещи не слишком плохими (хотя и не глобальными/универсальными) в терминах или удалении дублированного кода.

[WebMethod]
[ScriptMethod(UseHttpGet = true, ResponseFormat = ResponseFormat.Json)]
public String GetServerTime()
{
  return Execute(() => DateTime.Now.ToString());
}

public T Execute<T>(Func<T> action)
{
  if (action == null)
    throw new ArgumentNullException("action");

  try
  {
    return action.Invoke();
  }
  catch (Exception ex)
  {
    throw; // Do meaningful error handling/logging...
  }
}

Где Execute может быть реализовано в подклассе WebService или в качестве метода расширения.

UPDATE: Reflection Evil

Как уже упоминалось в моем первоначальном ответе, вы можете злоупотреблять размышлениями, чтобы получить то, что вы хотите... в частности, вы можете создать свой собственный HttpHandler, который использует внутренние компоненты RestHandler для предоставления точки перехвата для получения сведений об исключении. У меня есть пример "небезопасного" кода ниже, чтобы вы начали.

Лично я НЕ использовал бы этот код; но он работает.

namespace WebHackery
{
  public class AjaxServiceHandler : IHttpHandler
  {
    private readonly Type _restHandlerType;
    private readonly MethodInfo _createHandler;
    private readonly MethodInfo _getRawParams;
    private readonly MethodInfo _invokeMethod;
    private readonly MethodInfo _writeExceptionJsonString;
    private readonly FieldInfo _webServiceMethodData;

    public AjaxServiceHandler()
    {
      _restHandlerType = typeof(ScriptMethodAttribute).Assembly.GetType("System.Web.Script.Services.RestHandler");

      _createHandler = _restHandlerType.GetMethod("CreateHandler", BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(HttpContext) }, null);
      _getRawParams = _restHandlerType.GetMethod("GetRawParams", BindingFlags.NonPublic | BindingFlags.Static);
      _invokeMethod = _restHandlerType.GetMethod("InvokeMethod", BindingFlags.NonPublic | BindingFlags.Static);
      _writeExceptionJsonString = _restHandlerType.GetMethod("WriteExceptionJsonString", BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(HttpContext), typeof(Exception) }, null);

      _webServiceMethodData = _restHandlerType.GetField("_webServiceMethodData", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField);
    }

    public bool IsReusable
    {
      get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
      var restHandler = _createHandler.Invoke(null, new Object[] { context });
      var methodData = _webServiceMethodData.GetValue(restHandler);
      var rawParams = _getRawParams.Invoke(null, new[] { methodData, context });

      try
      {
        _invokeMethod.Invoke(null, new[] { context, methodData, rawParams });
      }
      catch (Exception ex)
      {
        while (ex is TargetInvocationException)
          ex = ex.InnerException;

        // Insert Custom Error Handling HERE...

        _writeExceptionJsonString.Invoke(null, new Object[] { context, ex});
      }
    }
  }
}

Ответ 2

Это хороший вопрос. У меня была эта проблема в веб-приложении, над которым я работаю. Боюсь, что мое решение не было таким уж умным. Я просто обернул код в каждом методе веб-службы в try/catch и возвратил объект, который указывал, был ли метод успешно запущен или нет. К счастью, у моего приложения нет большого количества вызовов веб-сервисов, поэтому я могу уйти от этого. Я ценю это не очень хорошее решение, если вы выполняете множество вызовов веб-сервисов.

Ответ 3

Вы взглянули на WCF Web API? В настоящее время он находится в режиме бета-тестирования, но он уже используется в нескольких сайтах. Выявление json/xml rest api очень просто. В нем вы можете создать свой собственный производный класс HttpErrorHandler для обработки ошибок. Это очень чистое решение. Возможно, стоит поближе познакомиться с вами. Миграция любого существующего сервиса также должна быть простой. Надеюсь, это поможет!

Ответ 4

Edit

Вы пытались отключить обработку ошибок в web.config, а затем добавили модуль ошибок?

<location path="Webservices" >
    <system.web>
     <customErrors mode="Off" />
    </system.web>
</location>

Нормальная обработка ошибок, поскольку я думаю, что это должно быть даже для RestHandler

Вы можете создать HttpModule, чтобы поймать все ошибки и вернуть пользовательский результат. Обратите внимание, что это сообщение об ошибке будет ответом клиенту, а не ожидаемым результатом веб-службы. Вы также можете бросить любое исключение custom в приложении и позволить модулю обрабатывать его, если клиент должен ответить или уведомить пользователя об этом.

Пример результата правильного выполнения:

{
  success: true,
  data: [ .... ],
  error: null
}

Результат неудачного выполнения:

{
  success: false,
  data: null,
  error: {
    message: 'error message',
    stackTrace: 'url encoded stack trace'
  }
}

Добавьте это в web.config:

<modules>
  <add name="UnhandledException" type="Modules.Log.UnhandledException"/>
</modules>

Вы можете легко добавить код, чтобы сделать саморегистрацию модуля, реализовав довольно недокументированную функцию PreApplicationStartMethod в модуле, и создайте и используйте свой собственный класс HttpApplication для использования вместо стандартного. Делая это, вы можете добавить любой модуль в приложение, просто добавив его в папку bin приложения. Это то, как я делаю все свои приложения в настоящее время, и я должен сказать, что он отлично работает.

Ниже код поймает все ошибки приложения и вернет html-ответ с сообщением об ошибке (вам нужно будет заменить html файл на вашу собственную реализацию сообщения об ошибке json):

using System.Web;

namespace Modules.Log
{
    using System;
    using System.Text;

    public class LogModule : IHttpModule
    {
        private bool initialized = false;
        private object initLock = new Object();
        private static string applicationName = string.Format("Server: {0}; [LogModule_AppName] installation name is not set", System.Environment.MachineName);

        public void Dispose()
        {
        }

        public void Init(HttpApplication context)
        {
            // Do this one time for each AppDomain.
            if (!initialized)
            {
                lock (initLock)
                {
                    if (!initialized)
                    {
                        context.Error += new EventHandler(this.OnUnhandledApplicationException);

                        initialized = true;
                    }
                }
            }
        }

        private void OnUnhandledApplicationException(object sender, EventArgs e)
        {
            StringBuilder message = new StringBuilder("<html><head><style>" +
                "body, table { font-size: 12px; font-family: Arial, sans-serif; }\r\n" +
                "table tr td { padding: 4px; }\r\n" +
                ".header { font-weight: 900; font-size: 14px; color: #fff; background-color: #2b4e74; }\r\n" +
                ".header2 { font-weight: 900; background-color: #c0c0c0; }\r\n" +
                "</style></head><body><table><tr><td class=\"header\">\r\n\r\nUnhandled Exception logged by LogModule.dll:\r\n\r\nappId=");

            string appId = (string)AppDomain.CurrentDomain.GetData(".appId");
            if (appId != null)
            {
                message.Append(appId);
            }

            message.Append("</td></tr>");

            HttpServerUtility server = HttpContext.Current.Server;
            Exception currentException = server.GetLastError();

            if (currentException != null)
            {
                message.AppendFormat(
                        "<tr><td class=\"header2\">TYPE</td></tr><tr><td>{0}</td></tr><tr><td class=\"header2\">REQUEST</td></tr><tr><td>{3}</td></tr><tr><td class=\"header2\">MESSAGE</td></tr><tr><td>{1}</td></tr><tr><td class=\"header2\">STACK TRACE</td></tr><tr><td>{2}</td></tr>",
                        currentException.GetType().FullName,
                        currentException.Message,
                        currentException.StackTrace,
                        HttpContext.Current != null ? HttpContext.Current.Request.FilePath : "n/a");
                server.ClearError();
            }

            message.Append("</table></body></html>");

            HttpContext.Current.Response.Write(message.ToString());
            server.ClearError();
        }
    }
}