失败/错误时,JSON服务应返回什么


79

我在C#(.ashx文件)中编写JSON服务。在成功请求服务后,我返回了一些JSON数据。如果请求失败,要么是由于引发异常(例如,数据库超时),要么是因为请求在某种程度上是错误的(例如,将数据库中不存在的ID用作参数),服务应该如何响应?哪些HTTP状态代码是明智的,是否应该返回任何数据?

我预计该服务将主要使用jQuery.form插件从jQuery调用,jQuery或此插件是否具有处理错误响应的默认方式?

编辑:我已经决定成功使用jQuery + .ashx + HTTP [状态码],我将返回JSON,但在错误时,我将返回一个字符串,因为这似乎是jQuery的错误选项。阿贾克斯期望。

Answers:


34

您返回的HTTP状态代码应取决于发生的错误的类型。如果数据库中不存在ID,则返回404;否则,返回404。如果用户没有足够的特权进行该Ajax调用,则返回403;否则,返回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-我同意数据库超时应为
500。– thatismatt

4
只是想指出404意味着所寻址的资源丢失。在这种情况下,资源是您的POST处理器,而不是数据库中具有ID的随机对象。在这种情况下400是更合适的。
StevenC 2012年

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不会“检测”错误代码,而是缺少成功代码。当尝试使用jQuery围绕http响应代码设计客户端时,这是一个重要的区别。您只有两种选择(“成功”还是“错误”?),您必须自己进一步选择。如果您的WebServices数量较少,而驱动的页面数量较少,则可以,但是任何规模较大的产品都可能变得混乱。

.asmxWebService(或WCF)中,返回自定义对象比自定义HTTP状态代码更为自然。另外,您还可以免费获得JSON序列化。


1
有效的方法,只有一个nitpick:示例不是有效的JSON(关键字名称缺少双引号)
StaxMan,2009年

1
这是我曾经做过的,但是您实际上应该使用http状态代码,这就是它们的作用(特别是如果您正在做RESTful的东西)
Eva 2012年

我认为这种方法绝对有效-HTTP状态代码在处理宁静的事情时很有用,但是当您对包含数据库查询的脚本进行api调用时却无济于事。即使数据库查询返回错误,http状态代码仍将为200。在这种情况下,我通常使用“成功”键来指示MySQL查询是否成功:)
Terry

17

使用HTTP状态代码将是RESTful的方式,但这建议您使用资源URI等使其余接口成为RESTful。

实际上,可以按自己的喜好定义接口(例如返回错误对象,详细说明错误的属性,以及解释该错误的大量HTML等),但是一旦确定了可以在原型中起作用的东西,要保持一致。


我喜欢您的建议,我假设您认为我应该返回JSON?诸如此类:{错误:{消息:“发生错误”,详细信息:“发生是因为它是星期一。”}}
thatismatt

@thatismatt —如果错误总是致命的,那是很合理的。要获得更高的粒度,制作error一个(可能为空)数组并添加一个fatal_error: bool参数将为您提供很大的灵活性。
本·布兰克

2
哦,为何时使用和何时不使用RESTful响应+1。:-)
Ben Blank

罗恩·德维拉(Ron DeVera)解释了我的想法!
Phil H

3

我认为,如果您只是冒泡一个异常,则应在为'error'选项传递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”错误返回的方式。我相信这是相当标准的。在处理潜在的意外错误情况时具有这种一致性绝对是一件好事。

另外,如果您使用C#编写,为什么不只使用.NET中的内置功能呢?WCF和ASMX服务使您可以轻松地将数据序列化为JSON,而无需重新设计轮子。


我认为在这种情况下不应使用500个错误代码。根据规范:w3.org/Protocols/rfc2616/rfc2616-sec10.html,最好的选择是发送400(错误请求)。500错误更适合于未处理的异常。
加布里埃尔·马泽托


2

是的,您应该使用HTTP状态代码。并且最好还以某种标准化的JSON格式返回错误描述,例如Nottingham的proposal,请参见适应性错误报告

API问题的有效负载具有以下结构:

  • type:描述错误情况的文档的URL(可选,如果未提供,则假定为“ about:blank”;应解析为人类可读的文档; Apigility始终提供此信息)。
  • title:错误条件的简短标题(必填;对于相同类型的每个问题,标题应相同; Apigility始终提供该标题)。
  • status:当前请求的HTTP状态代码(可选; Apigility始终提供此代码)。
  • detail:特定于此请求的错误详细信息(可选;对于每个问题,适应性都需要它)。
  • instance:URI,标识此问题的特定实例(可选; Apigility当前不提供此功能)。

1

如果用户提供了无效数据,则绝对应为400 Bad Request请求包含错误的语法或无法实现。


400范围中的任何一个都是可以接受的,而422是无法处理的数据的最佳选择
jamesc

0

我认为您不应该返回任何http错误代码,而是返回对应用程序客户端有用的自定义异常,以便接口知道实际发生了什么。我不会尝试用404错误代码或类似性质的代码掩盖实际问题。


您是否建议即使出现问题也要返回200?您是什么意思“自定义例外”?您是说一段描述错误的JSON吗?
thatismatt

4
布拉赫,返回http代码并不意味着您也无法返回错误描述消息。返回200真是愚蠢,更何况是错误的。
StaxMan

同意@StaxMan-始终返回最佳状态代码,但在返回信息中包含说明
schmoopy 2013年

0

对于服务器/协议错误,我会尽量使用REST / HTTP(与您在浏览器中输入URL进行比较):

  • 一个不存在的项目称为(/ persons / {non-existing-id-here})。返回404。
  • 服务器上发生意外错误(代码错误)。返回500。
  • 客户端用户无权获取资源。返回401。

对于特定于域/业务逻辑的错误,我会说该协议使用了正确的方式,并且没有服务器内部错误,因此请使用错误JSON / XML对象或您希望用其描述数据的任何内容进行响应(在填写时进行比较)网站上的表格):

  • 用户想要更改其帐户名,但尚未通过单击发送给用户的电子邮件中的链接来验证其帐户。返回{“错误”:“帐户未通过验证”}或其他。
  • 用户想订购一本书,但该书已售出(状态在DB中已更改),因此无法再订购。返回{“ error”:“书籍已售出”}。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.