Що повинна повертати служба JSON у разі помилки / помилки


79

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

Я передбачаю, що служба буде в основному викликатися з jQuery за допомогою плагіна jQuery.form, чи має jQuery або цей плагін якийсь спосіб за замовчуванням для обробки відповіді на помилку?

РЕДАГУВАТИ: Я вирішив використовувати jQuery + .ashx + HTTP [коди стану] після успіху, я поверну JSON, але при помилці я поверну рядок, оскільки, здається, це те, що варіант помилки для jQuery. ajax очікує.

Відповіді:


34

Код стану 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
  }
});

3
Як ви вважаєте, який код помилки я повинен повернути, якщо хтось надає недійсні дані, наприклад рядок, де потрібно ціле число? або недійсна електронна адреса?
thatismatt

щось у діапазоні 500, те саме, що будь-яка подібна помилка коду на стороні сервера
annakata

7
Діапазон 500 - це помилка сервера, але на сервері нічого не пішло. Вони зробили поганий запит, то чи не повинно це бути в діапазоні 400?
thatismatt

38
Як користувач, якщо я отримую 500, я знаю, що я не винен, якщо отримаю 400, я можу зрозуміти, що я зробив неправильно, це особливо важливо при написанні API, оскільки ваші користувачі технічно грамотні та каже їм правильно використовувати 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..." }

ТО викине виняток із сервера ( 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. У цьому випадку я зазвичай використовую клавішу "успіх", щоб вказати, чи був запит MySQL успішним чи ні :)
Террі

17

Використання кодів статусу HTTP було б найпопулярнішим способом зробити це, але це означало б, що ви зробите решту інтерфейсу RESTful за допомогою URI ресурсів тощо.

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


Мені подобається те, що ви пропонуєте, я припускаю, що ви думаєте, що я повинен повернути JSON тоді? Щось на кшталт: {error: {message: "Сталася помилка", подробиці: "Це сталося через те, що сьогодні понеділок."}}
thatismatt

@thatismatt - Це цілком розумно, якщо помилки завжди фатальні. Для більшої деталізації, створення error(можливо порожнього) масиву та додавання fatal_error: boolпараметра дасть вам трохи гнучкості.
Бен Бланк

2
О, і +1 для відповідей RESTful, коли використовувати, а коли не використовувати. :-)
Бен Бланк

Рон ДеВера пояснив, про що я думав!
Phil H

3

Я думаю, що якщо ви просто додаєте виняток, це має бути оброблено у зворотному виклику jQuery, який передається для опції "помилка" . (Ми також реєструємо цей виняток на стороні сервера до центрального журналу). Спеціальний код помилки HTTP не потрібен, але мені цікаво подивитися, що роблять і інші люди.

Це те, що я роблю, але це лише мої $ .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);

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


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

2

Я б точно повернув помилку 500 із об'єктом JSON, що описує стан помилки, подібно до того, як повертається помилка ASP.NET AJAX "ScriptService" . Я вважаю, що це досить стандартно. Безумовно, приємно мати таку послідовність при обробці потенційно несподіваних умов помилок.

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


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

2

Рейкові ліси використовують 422 Unprocessable Entityдля таких видів помилок. Дивіться RFC 4918 для отримання додаткової інформації.


2

Так, вам слід використовувати коди стану HTTP. А також бажано повертати описи помилок у дещо стандартизованому форматі JSON, наприклад , пропозиція Ноттінгема , див. Звітування про помилки щодо зручності :

Корисне навантаження проблеми API має таку структуру:

  • тип : URL-адреса документа, що описує стан помилки (необов’язково, і передбачається «about: blank», якщо жодного не вказано; має перейти до документа, що читається людиною ; Apigility завжди надає це).
  • title : короткий заголовок умови помилки (обов’язково; і повинен бути однаковим для кожної проблеми одного типу ; спритність завжди це передбачає).
  • status : код стану HTTP для поточного запиту (необов’язково; це завжди надає Apigility).
  • деталь : деталі помилок, специфічні для цього запиту (необов’язково; спритність вимагає цього для кожної проблеми).
  • екземпляр : 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-адреси у своєму браузері):

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

Для помилок, пов'язаних із доменом / бізнес-логікою, я б сказав, що протокол використовується правильно, і немає внутрішньої помилки сервера, тому відповідайте помилкою на об'єкт JSON / XML або на те, чим ви бажаєте описувати свої дані (порівняйте це, заповнивши форми на веб-сайті):

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