Что должен возвращать сервис JSON при сбое / ошибке - PullRequest
77 голосов
/ 23 марта 2009

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

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

РЕДАКТИРОВАТЬ: Я решил, что в случае успеха я буду использовать jQuery + .ashx + HTTP [коды состояния]. Я верну JSON, но при ошибке я верну строку, так как кажется, что это то, что ожидает опция ошибки для jQuery.ajax.

Ответы [ 11 ]

55 голосов
/ 23 марта 2009

См. этот вопрос , чтобы получить представление о передовых практиках в вашей ситуации.

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

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

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

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

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

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

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

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

33 голосов
/ 23 марта 2009

Код состояния 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
  }
});
17 голосов
/ 23 марта 2009

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

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

3 голосов
/ 03 августа 2012

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

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

Я создаю HandleErrorAttribute (подробности см. В комментариях к коду). Некоторые детали, включая «использование», были пропущены, поэтому код может не скомпилироваться. Я добавляю фильтр к глобальным фильтрам во время инициализации приложения в 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's 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's 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);

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

3 голосов
/ 23 марта 2009

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

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

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

2 голосов
/ 15 января 2016

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

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

  • type : URL-адрес документа, описывающий состояние ошибки (необязательно, а about: blank) предполагается, что ничего не указано; следует преобразовать в читаемый человеком документ; Ловкость всегда обеспечивает это).
  • title : краткое название для условия ошибки (обязательно; должно быть одинаковым для всех Задача того же типа ; Apigility всегда обеспечивает это).
  • status : код состояния HTTP для текущего запроса (необязательно; Apigility всегда предоставляет это).
  • detail : подробности ошибки, характерные для этого запроса (необязательно; Apigility требует его для каждого проблема).
  • instance : URI, идентифицирующий конкретный экземпляр этой проблемы (необязательно; Apigility в настоящее время не обеспечивает это).
2 голосов
/ 27 мая 2011

Rails-леса используют 422 Unprocessable Entity для таких ошибок. См. RFC 4918 для получения дополнительной информации.

2 голосов
/ 23 марта 2009

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

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

1 голос
/ 06 августа 2009

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

0 голосов
/ 18 июля 2013

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

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

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

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