Что должна вернуть служба JSON при сбое/ошибке

Я пишу службу JSON в С# (файл .ashx). При успешном запросе услуги я возвращаю некоторые данные JSON. Если запрос завершился неудачно, либо из-за того, что было выбрано исключение (например, время ожидания базы данных), либо потому, что запрос был некорректным (например, идентификатор, который не существует в базе данных, был указан как аргумент), как должна отвечать служба? Какие коды состояния HTTP являются разумными и должны ли я возвращать какие-либо данные, если они есть?

Я ожидаю, что эта услуга будет в основном вызвана из jQuery с использованием плагина jQuery.form, имеет ли jQuery или этот плагин какой-либо способ по умолчанию для обработки ответа об ошибке?

EDIT: Я решил использовать jQuery +.ashx + HTTP [коды статуса] при успешном возвращении JSON, но при ошибке я верну строку, как она появляется это то, что ожидает ошибка для jQuery.ajax.

Ответ 1

Возвращаемый код состояния HTTP должен зависеть от типа ошибки, которая произошла. Если идентификатор не существует в базе данных, верните 404; если пользователь не имеет достаточных привилегий для совершения этого вызова Ajax, верните 403; если база данных истекает раньше, чем можно найти запись, верните 500 (ошибка сервера).

jQuery автоматически обнаруживает такие коды ошибок и запускает функцию обратного вызова, которую вы определяете в своем вызове Ajax. Документация: http://api.jquery.com/jQuery.ajax/

Краткий пример обратного вызова ошибки $.ajax:

$.ajax({
  type: 'POST',
  url: '/some/resource',
  success: function(data, textStatus) {
    // Handle success
  },
  error: function(xhr, textStatus, errorThrown) {
    // Handle error
  }
});

Ответ 2

См. этот вопрос для ознакомления с передовыми методами для вашей ситуации.

Предложение линии верха (из указанной ссылки) заключается в стандартизации структуры ответа (как для успеха, так и неудачи), которую ищет ваш обработчик, вылавливания всех Исключений на уровне сервера и преобразования их в одну и ту же структуру. Например (из этого ответа):

{
    success:false,
    general_message:"You have reached your max number of Foos for the day",
    errors: {
        last_name:"This field is required",
        mrn:"Either SSN or MRN must be entered",
        zipcode:"996852 is not in Bernalillo county. Only Bernalillo residents are eligible"
    }
} 

Это используется метод stackoverflow подхода (в случае, если вам интересно, как это делают другие); такие как голосование имеют поля "Success" и "Message", независимо от того, разрешено ли голосование или нет:

{ Success:true, NewScore:1, Message:"", LastVoteTypeId:3 }

Как отметил @Phil.H, вы должны быть в соответствии с тем, что вы выберете. Это легче сказать, чем сделать (как и все в развитии!).

Например, если вы отправляете комментарии слишком быстро на SO, вместо того, чтобы быть последовательными и возвращать

{ Success: false, Message: "Can only comment once every blah..." }

SO будет генерировать исключение сервера (HTTP 500) и уловить его в обратном вызове error.

Насколько он "чувствует себя хорошо" для использования jQuery + .ashx + HTTP [коды статуса] IMO, он добавит больше сложности для вашей клиентской базы кода, чем это стоит. Поймите, что jQuery не "обнаруживает" коды ошибок, а скорее отсутствие кода успеха. Это важное различие при попытке спроектировать клиент вокруг кодов ответов HTTP с помощью jQuery. У вас есть только два варианта (было ли это "успехом" или "ошибкой"?), Которые вам нужно развернуть дальше самостоятельно. Если у вас небольшое количество WebServices, управляющих небольшим количеством страниц, тогда это может быть хорошо, но что-то в более крупном масштабе может стать беспорядочным.

Это гораздо более естественно в .asmx WebService (или WCF, если на то пошло) для возврата пользовательского объекта, чем для настройки кода состояния HTTP. Кроме того, вы бесплатно получаете сериализацию JSON.

Ответ 3

Использование кодов состояния HTTP было бы RESTful способом сделать это, но это предложило бы сделать остальную часть интерфейса RESTful с использованием URI ресурсов и так далее.

По правде говоря, определите интерфейс по своему усмотрению (верните объект ошибки, например, детализируя свойство с ошибкой и кусок HTML, который его объясняет, и т.д.), но как только вы решите что-то, что работает в прототипе, быть безжалостным.

Ответ 4

Я потратил несколько часов на решение этой проблемы. Мое решение основано на следующих пожеланиях/требованиях:

  • Не повторяйте код обработки ошибок с помощью шаблонов во всех действиях контроллера JSON.
  • Сохранять коды состояния HTTP (ошибки). Зачем? Поскольку проблемы более высокого уровня не должны влиять на реализацию более низкого уровня.
  • Уметь получать данные JSON при возникновении ошибки/исключения на сервере. Зачем? Потому что я могу получить богатую информацию об ошибках. Например. сообщение об ошибке, код состояния ошибки домена, трассировка стека (в среде отладки/разработки).
  • Простота использования на стороне клиента - предпочтительнее использовать jQuery.

Я создаю HandleErrorAttribute (см. комментарии к коду для объяснения деталей). Несколько деталей, включая "usings", были опущены, поэтому код не может компилироваться. Я добавляю фильтр к глобальным фильтрам во время инициализации приложения в Global.asax.cs следующим образом:

GlobalFilters.Filters.Add(new UnikHandleErrorAttribute());

Атрибут

namespace Foo
{
  using System;
  using System.Diagnostics;
  using System.Linq;
  using System.Net;
  using System.Reflection;
  using System.Web;
  using System.Web.Mvc;

  /// <summary>
  /// Generel error handler attribute for Foo MVC solutions.
  /// It handles uncaught exceptions from controller actions.
  /// It outputs trace information.
  /// If custom errors are enabled then the following is performed:
  /// <ul>
  ///   <li>If the controller action return type is <see cref="JsonResult"/> then a <see cref="JsonResult"/> object with a <c>message</c> property is returned.
  ///       If the exception is of type <see cref="MySpecialExceptionWithUserMessage"/> it message will be used as the <see cref="JsonResult"/> <c>message</c> property value.
  ///       Otherwise a localized resource text will be used.</li>
  /// </ul>
  /// Otherwise the exception will pass through unhandled.
  /// </summary>
  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
  public sealed class FooHandleErrorAttribute : HandleErrorAttribute
  {
    private readonly TraceSource _TraceSource;

    /// <summary>
    /// <paramref name="traceSource"/> must not be null.
    /// </summary>
    /// <param name="traceSource"></param>
    public FooHandleErrorAttribute(TraceSource traceSource)
    {
      if (traceSource == null)
        throw new ArgumentNullException(@"traceSource");
      _TraceSource = traceSource;
    }

    public TraceSource TraceSource
    {
      get
      {
        return _TraceSource;
      }
    }

    /// <summary>
    /// Ctor.
    /// </summary>
    public FooHandleErrorAttribute()
    {
      var className = typeof(FooHandleErrorAttribute).FullName ?? typeof(FooHandleErrorAttribute).Name;
      _TraceSource = new TraceSource(className);
    }

    public override void OnException(ExceptionContext filterContext)
    {
      var actionMethodInfo = GetControllerAction(filterContext.Exception);
      // It probably an error if we cannot find a controller action. But, hey, what should we do about it here?
      if(actionMethodInfo == null) return;

      var controllerName = filterContext.Controller.GetType().FullName; // filterContext.RouteData.Values[@"controller"];
      var actionName = actionMethodInfo.Name; // filterContext.RouteData.Values[@"action"];

      // Log the exception to the trace source
      var traceMessage = string.Format(@"Unhandled exception from {0}.{1} handled in {2}. Exception: {3}", controllerName, actionName, typeof(FooHandleErrorAttribute).FullName, filterContext.Exception);
      _TraceSource.TraceEvent(TraceEventType.Error, TraceEventId.UnhandledException, traceMessage);

      // Don't modify result if custom errors not enabled
      //if (!filterContext.HttpContext.IsCustomErrorEnabled)
      //  return;

      // We only handle actions with return type of JsonResult - I don't use AjaxRequestExtensions.IsAjaxRequest() because ajax requests does NOT imply JSON result.
      // (The downside is that you cannot just specify the return type as ActionResult - however I don't consider this a bad thing)
      if (actionMethodInfo.ReturnType != typeof(JsonResult)) return;

      // Handle JsonResult action exception by creating a useful JSON object which can be used client side
      // Only provide error message if we have an MySpecialExceptionWithUserMessage.
      var jsonMessage = FooHandleErrorAttributeResources.Error_Occured;
      if (filterContext.Exception is MySpecialExceptionWithUserMessage) jsonMessage = filterContext.Exception.Message;
      filterContext.Result = new JsonResult
        {
          Data = new
            {
              message = jsonMessage,
              // Only include stacktrace information in development environment
              stacktrace = MyEnvironmentHelper.IsDebugging ? filterContext.Exception.StackTrace : null
            },
          // Allow JSON get requests because we are already using this approach. However, we should consider avoiding this habit.
          JsonRequestBehavior = JsonRequestBehavior.AllowGet
        };

      // Exception is now (being) handled - set the HTTP error status code and prevent caching! Otherwise you'll get an HTTP 200 status code and running the risc of the browser caching the result.
      filterContext.ExceptionHandled = true;
      filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; // Consider using more error status codes depending on the type of exception
      filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);

      // Call the overrided method
      base.OnException(filterContext);
    }

    /// <summary>
    /// Does anybody know a better way to obtain the controller action method info?
    /// See http://stackoverflow.com/questions/2770303/how-to-find-in-which-controller-action-an-error-occurred.
    /// </summary>
    /// <param name="exception"></param>
    /// <returns></returns>
    private static MethodInfo GetControllerAction(Exception exception)
    {
      var stackTrace = new StackTrace(exception);
      var frames = stackTrace.GetFrames();
      if(frames == null) return null;
      var frame = frames.FirstOrDefault(f => typeof(IController).IsAssignableFrom(f.GetMethod().DeclaringType));
      if (frame == null) return null;
      var actionMethod = frame.GetMethod();
      return actionMethod as MethodInfo;
    }
  }
}

Я разработал следующий плагин jQuery для удобства использования на стороне клиента:

(function ($, undefined) {
  "using strict";

  $.FooGetJSON = function (url, data, success, error) {
    /// <summary>
    /// **********************************************************
    /// * UNIK GET JSON JQUERY PLUGIN.                           *
    /// **********************************************************
    /// This plugin is a wrapper for jQuery.getJSON.
    /// The reason is that jQuery.getJSON success handler doesn't provides access to the JSON object returned from the url
    /// when a HTTP status code different from 200 is encountered. However, please note that whether there is JSON
    /// data or not depends on the requested service. if there is no JSON data (i.e. response.responseText cannot be
    /// parsed as JSON) then the data parameter will be undefined.
    ///
    /// This plugin solves this problem by providing a new error handler signature which includes a data parameter.
    /// Usage of the plugin is much equal to using the jQuery.getJSON method. Handlers can be added etc. However,
    /// the only way to obtain an error handler with the signature specified below with a JSON data parameter is
    /// to call the plugin with the error handler parameter directly specified in the call to the plugin.
    ///
    /// success: function(data, textStatus, jqXHR)
    /// error: function(data, jqXHR, textStatus, errorThrown)
    ///
    /// Example usage:
    ///
    ///   $.FooGetJSON('/foo', { id: 42 }, function(data) { alert('Name :' + data.name); }, function(data) { alert('Error: ' + data.message); });
    /// </summary>

    // Call the ordinary jQuery method
    var jqxhr = $.getJSON(url, data, success);

    // Do the error handler wrapping stuff to provide an error handler with a JSON object - if the response contains JSON object data
    if (typeof error !== "undefined") {
      jqxhr.error(function(response, textStatus, errorThrown) {
        try {
          var json = $.parseJSON(response.responseText);
          error(json, response, textStatus, errorThrown);
        } catch(e) {
          error(undefined, response, textStatus, errorThrown);
        }
      });
    }

    // Return the jQueryXmlHttpResponse object
    return jqxhr;
  };
})(jQuery);

Что я получу от всего этого? Конечным результатом является то, что

  • Ни одно из моих действий с контроллером не имеет требований к HandleErrorAttributes.
  • Ни один из моих действий с контроллером не содержит повторяющегося кода обработки ошибок котла.
  • У меня есть одна точка кода обработки ошибок, которая позволяет мне легко изменять ведение журнала и другие связанные с обработкой ошибок вещи.
  • Простое требование: Действия контроллера, возвращающие JsonResult, должны иметь тип возврата JsonResult, а не некоторый базовый тип, например ActionResult. Причина: см. Комментарий к коду в FooHandleErrorAttribute.

Пример на стороне клиента:

var success = function(data) {
  alert(data.myjsonobject.foo);
};
var onError = function(data) {
  var message = "Error";
  if(typeof data !== "undefined")
    message += ": " + data.message;
  alert(message);
};
$.FooGetJSON(url, params, onSuccess, onError);

Комментарии приветствуются! Вероятно, я расскажу об этом решении однажды...

Ответ 5

Я думаю, что если вы просто создаете исключение, его следует обрабатывать в обратном вызове jQuery, который передается для опции "ошибка" ., (Мы также регистрируем это исключение на стороне сервера в центральном журнале). Никакого специального кода ошибки HTTP не требуется, но мне любопытно посмотреть, что делают другие люди.

Это то, что я делаю, но это только мои $.02

Если вы собираетесь быть RESTful и возвращать коды ошибок, попробуйте придерживаться стандартных кодов, заданных W3C: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

Ответ 6

Я определенно вернул бы ошибку 500 с объектом JSON, описывающим условие ошибки, похожее на как ошибка ASP.NET AJAX "ScriptService" возвращает. Я считаю, что это довольно стандартно. Это определенно приятно иметь такую ​​согласованность при работе с потенциально неожиданными условиями ошибки.

Кроме того, почему бы просто не использовать встроенные функции в .NET, если вы пишете его на С#? Услуги WCF и ASMX упрощают сериализацию данных как JSON, не изобретая колесо.

Ответ 7

Рельсы для лесов используют 422 Unprocessable Entity для таких ошибок. Подробнее см. RFC 4918.

Ответ 8

Если пользователь поставляет неверные данные, он должен определенно быть 400 Bad Request (запрос содержит плохой синтаксис или не может быть выполнен.)

Ответ 9

Я не думаю, что вам следует возвращать любые коды ошибок HTTP, а также пользовательские исключения, которые полезны для клиентской части приложения, поэтому интерфейс знает, что на самом деле произошло. Я бы не пытался маскировать реальные проблемы с 404 кодами ошибок или что-то в этом роде.

Ответ 10

Для ошибок сервера/протокола я старался быть как можно REST/HTTP (сравните это с тем, как вы вводите URL-адрес в своем браузере):

  • вызывается не существующий элемент (/persons/{non-existing-id-here}). Верните 404.
  • произошла непредвиденная ошибка на сервере (ошибка кода). Верните 500.
  • Пользователь клиента не имеет права на получение ресурса. Верните 401.

Для конкретных ошибок в домене/бизнес-логике я бы сказал, что протокол используется правильно, и нет внутренней ошибки сервера, поэтому ответьте на объект JSON/XML с ошибкой или все, что вы предпочитаете описывать ваши данные (Сравните это с вы заполняете формы на веб-сайте):

  • пользователь хочет изменить имя своей учетной записи, но пользователь еще не подтвердил свою учетную запись, щелкнув ссылку в электронном письме, которое было отправлено пользователю. Return { "error": "Account not verified" } или что-то еще.
  • пользователь хочет заказать книгу, но книга была продана (состояние изменено в БД) и больше не может быть заказано. Return { "error": "Книга уже продана" }.

Ответ 11

Да, вы должны использовать коды состояния HTTP. А также предпочтительно возвращать описания ошибок в несколько стандартизованном формате JSON, например предложение Ноттингемса, см. Отчет об ошибках apigility:

Полезная нагрузка задачи API имеет следующую структуру:

  • тип: URL-адрес документа, описывающего условие ошибки (необязательно, и "about: blank" - если не предусмотрено; должен разрешаться для удобочитаемого документа; Привилегия всегда обеспечивает это).
  • title: краткое название условия ошибки (обязательно; и должно быть одинаковым для каждого проблема того же типа ; Apigility всегда обеспечивает это).
  • статус: код состояния HTTP для текущего запроса (необязательно, Apigility всегда предоставляет это).
  • подробно: сведения об ошибках, специфичные для этого запроса (необязательно; Apigility требует его для каждого проблема).
  • экземпляр: URI, определяющий конкретный экземпляр этой проблемы (необязательно; Apigility в настоящее время не предоставляет этого).