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

时间:2009-03-23 16:15:49

标签: jquery web-services json

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

我预计服务将主要使用jQuery.form插件从jQuery调用,jQuery或者这个插件是否有任何默认的方式来处理错误响应?

编辑:我已经决定成功使用jQuery + .ashx + HTTP [状态代码]我会返回JSON但是出错时我会返回一个字符串,因为它出现了这就是jQuery.ajax的错误选项所期望的。

11 个答案:

答案 0 :(得分:55)

请参阅this question,了解您所处情况的最佳做法。

topline建议(来自所述链接)是标准化处理程序查找的响应结构(成功和失败),捕获服务器层的所有异常并将它们转换为相同的结构。例如(来自this answer):

{
    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 pointed out,无论你选择什么,都应该保持一致。这说起来容易做起来就像开发中的一切一样!

例如,如果您在SO上过快地提交评论,而不是保持一致并返回

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

SO将抛出服务器异常(HTTP 500)并在error回调中捕获它。

尽管使用jQuery + .ashx + HTTP [状态代码] IMO“感觉正确”,但它会增加客户端代码库的复杂性,而不是它的价值。意识到jQuery不会“检测”错误代码,而是缺少成功代码。当尝试使用jQuery围绕http响应代码设计客户端时,这是一个重要的区别。你只有两个选择(是“成功”还是“错误”?),你必须自己进一步分支。如果您有少量WebServices驱动少量页面,那么它可能没问题,但任何规模较大的可能都会变得混乱。

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

答案 1 :(得分:32)

您返回的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
  }
});

答案 2 :(得分:16)

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

实际上,您可以根据需要定义界面(例如,返回错误对象,详细说明带有错误的属性,以及解释它的一大块HTML等),但是一旦确定了可行的内容在原型中,要无情地保持一致。

答案 3 :(得分:3)

我认为如果您只是冒出异常,应该在jQuery callback that is passed in for the 'error' option处理。 (我们还将服务器端的此异常记录到中央日志中)。不需要特殊的HTTP错误代码,但我很想知道其他人也会这样做。

这就是我所做的,但这只是我的$ .02

如果您要成为RESTful并返回错误代码,请尝试遵守W3C规定的标准代码:http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

答案 4 :(得分: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);

非常欢迎评论!我有一天可能会在博客上发表这个解决方案...

答案 5 :(得分:2)

我肯定会使用描述错误条件的JSON对象返回500错误,类似于how an ASP.NET AJAX "ScriptService" error returns。我相信这是相当标准的。在处理潜在的意外错误情况时,保持这种一致性非常好。

除此之外,为什么不在.NET中使用内置功能,如果你用C#编写它? WCF和ASMX服务可以轻松地将数据序列化为JSON,而无需重新发明轮子。

答案 6 :(得分:2)

Rails脚手架使用422 Unprocessable Entity来处理这类错误。有关详细信息,请参阅RFC 4918

答案 7 :(得分:2)

是的,您应该使用HTTP状态代码。并且最好以某种标准化的JSON格式返回错误描述,例如Nottingham’s proposal,请参阅apigility Error Reporting

  

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

     
      
  •   类型:描述错误条件的文档的URL(可选,&#34; about:blank&#34;是   假设没有提供;应解析为人类可读的文档; Apigility总是   提供这个)。
  •   
  •   标题:错误条件的简要标题(必填;并且每个都应该相同)   同一类型的问题; Apigility总是提供这个)。
  •   
  •   状态:当前请求的HTTP状态代码(可选; Apigility始终提供此功能)。
  •   
  •   详细信息:此请求特定的错误详细信息(可选; Apigility要求每个请求   问题)。
  •   
  •   实例:标识此问题的特定实例的URI(可选;当前Apigility   不提供这个)。
  •   

答案 8 :(得分:1)

如果用户提供无效数据,则肯定应为400 Bad Request请求包含错误的语法或无法完成。

答案 9 :(得分:0)

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

答案 10 :(得分:0)

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

  • 调用非现有项目(/ persons / {non-existing-id-here})。返回404。
  • 发生服务器上的意外错误(代码错误)。返回500。
  • 客户端用户无权获取资源。返回401。

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

  • 用户想要更改其帐户名称,但用户尚未通过单击发送给用户的电子邮件中的链接来验证其帐户。返回{“错误”:“帐户未经过验证”}或其他。
  • 用户想要订购一本书,但该书已售出(状态在DB中更改),无法再订购。返回{“错误”:“已售出的书籍”}。