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


79

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

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

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

Ответы:


34

Код состояния HTTP, который вы возвращаете, должен зависеть от типа возникшей ошибки. Если ID не существует в базе данных, верните 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
  }
});

3
Как вы думаете, какой код ошибки я должен вернуть, если кто-то предоставит недопустимые данные, например строку, в которой требуется целое число? или неверный адрес электронной почты?
thatismatt

что-то в диапазоне 500, то же самое, что и любая аналогичная ошибка кода на стороне сервера
annakata

7
Диапазон 500 - это ошибка сервера, но на сервере ничего не произошло. Они сделали плохой запрос, так разве не должно быть в диапазоне 400?
thatismatt

38
Как пользователь, если я получаю 500, я знаю, что я не виноват, если я получаю 400, я могу понять, что я сделал не так, это особенно важно при написании API, поскольку ваши пользователи технически грамотны и 400 говорит им правильно использовать API. PS - Я согласен с тем, что таймаут DB должен быть 500.
thatismatt

4
Просто хочу отметить, что 404 означает, что адресный ресурс отсутствует. В этом случае ресурс - это ваш POST-процессор, а не какая-то случайная вещь в вашей БД с идентификатором. В этом случае более уместно 400.
StevenC

56

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

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

{
    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, управляющих небольшим количеством страниц, это может быть нормально, но что-то более крупное может стать беспорядочным.

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


1
Правильный подход, только одна придирка: примеры недействительны JSON (отсутствуют двойные кавычки для имен ключей)
StaxMan

1
это то, что я делал раньше, но вам действительно следует использовать коды статуса http, вот для чего они нужны (особенно если вы делаете вещи RESTful)
Ева

Я думаю, что этот подход определенно верен - коды состояния http полезны для успокоения, но не так полезны, когда, скажем, вы делаете вызовы api для сценария, который содержит запрос к базе данных. Даже когда запрос к базе данных возвращает ошибку, код состояния http все равно будет 200. В этом случае я обычно использую ключ «success», чтобы указать, был ли запрос MySQL успешным или нет :)
Терри,

17

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

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


Мне нравится то, что вы предлагаете, я предполагаю, что вы думаете, что тогда я должен вернуть JSON? Что-то вроде: {error: {message: «Произошла ошибка», подробности: «Это произошло, потому что сегодня понедельник».}}
thatismatt

@thatismatt - Это вполне разумно, если ошибки всегда фатальны. Для большей детализации создание error(возможно, пустого) массива и добавление fatal_error: boolпараметра даст вам некоторую гибкость.
Бен Бланк,

2
Да, и +1 для ответов RESTful, когда использовать, а когда не использовать. :-)
Бен Бланк

Рон ДеВера объяснил, о чем я думал!
Phil H

3

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

Я так и делаю, но это всего лишь мои 0,02 доллара.

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


3

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

  • Не используйте повторяющийся шаблонный код обработки ошибок во всех действиях контроллера 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);

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


буоо! Лучше иметь простой ответ, содержащий только необходимое объяснение, чем обширный ответ ради удовлетворения конкретной ситуации. в следующий раз
дайте

2

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

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


Я не думаю, что в этом контексте следует использовать код ошибки 500. Согласно спецификации: w3.org/Protocols/rfc2616/rfc2616-sec10.html , лучшей альтернативой является отправка 400 ( неверный запрос). Ошибка 500 больше подходит для необработанного исключения.
Габриэль Мазетто,

2

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


2

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

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

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

1

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


ЛЮБОЙ из диапазона 400 приемлем, а 422 - лучший вариант для данных, которые не могут быть обработаны
jamesc

0

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


Вы предлагаете мне вернуть 200, даже если что-то пойдет не так? Что вы имеете в виду под «настраиваемым исключением»? Вы имеете в виду кусок JSON с описанием ошибки?
thatismatt

4
Мля, возврат http-кода не означает, что вы ТАКЖЕ не можете вернуть сообщение с описанием ошибки. Вернуть 200 было бы довольно глупо, если не сказать неправильно.
StaxMan,

Согласен с @StaxMan - всегда возвращать лучший код статуса, но включать описание в возвращаемую информацию
schmoopy

0

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

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

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

  • пользователь хочет изменить имя своей учетной записи, но пользователь еще не подтвердил свою учетную запись, щелкнув ссылку в электронном письме, которое было отправлено пользователю. Верните {"error": "Account not verify"} или что-то еще.
  • пользователь хочет заказать книгу, но книга была продана (состояние изменилось в БД) и больше не может быть заказана. Вернуть {"error": "Книга уже продана"}.
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.