WebAPI错误处理:如何简化调试

什么是好的API错误?
易于使用的应用程序考虑了许多因素。其中之一是向他们的消费者提供有用的和描述性的错误。一个很好的起点就是看看标准。因为大部分网络都使用JSON和休息,我们将重点使用为JSON API。更具体地说,我们将查看我们在大多数WebAPI的-4xx和5xx错误中看到的典型错误代码。

良好的API错误区分客户端和服务器错误
最基本的错误划分标识问题是由客户端错误还是服务器错误造成的。

4xx级错误是客户端错误。这些是客户端可以通过更改他们的请求自行解决的错误。然后5xx错误是服务器错误,它向客户端表明它们可能没有做错什么,应该重试或联系支持。

这是一个基本的部门。不幸的是,有时应用程序会出错。无论是快到最后期限,还是没有一个好的错误处理模板可遵循,错误确实会发生。因此,您想要做的(我们将在后面讨论)是确保您的应用程序设置了全局异常处理和筛选,以便为您的使用者提供适当的代码。

良好的api错误适当地使用状态代码。
我们大多数人首先遇到的错误之一是臭名昭著的HTTP404状态代码。对消费者来说,这是一个明确的指标,表明他们寻找的东西并不存在。但是,您可能也看到了大量错误代码列表,您不太熟悉这些代码。

你怎么知道该用哪一种?你要具体到什么程度?

提示1:坚持使用著名的密码。
我的建议是不要尝试使用所有可用的错误代码。有一些基本的,大多数开发人员都知道和能够识别。而其他的则更晦涩,这会导致混乱。

那么我们应该使用哪些错误代码呢?好吧,我建议你至少熟悉其中的一些。下面列出的是我在调用API时偶尔会看到的内容。因此,我希望应用程序开发人员在正确的条件下生成它们。

电码 描述 注记
400 不良请求 这是一个通用错误,它告诉我们有人创建了一个错误的请求。可能缺少必需字段或没有填充标头值。
401 未经授权 指示身份验证失败。这可能是由于过期、丢失或无效令牌造成的。
403 禁 指示授权失败。或者,您也可以使用404,这样使用者甚至不知道资源是否存在。
404 找不到 找不到请求的资源。像GitHub这样的公司也使用404,如果您试图访问没有权限访问的资源。
500 内部服务器错误 当服务器出现故障时,消费者不能对此做任何事情。只要让他们知道有问题,他们应该再试一次或联系支持。
现在,您可能已经注意到,我没有包括代码,如405-方法不允许或503-服务不可用。这些是常见的代码,但我不希望应用程序开发人员生成它们。它们要么已由框架、服务器或网络组件处理。

技巧2:避免你不懂的代码
当许多开发人员第一次了解所有可用的HTTP状态代码时,他们会变得有些过于热心。他们希望对每一种情况都使用绝对最正确的代码。然而,这导致了它们的应用中不必要的复杂性。它还会导致消费应用程序修改它们的逻辑,并解释可能返回的更多代码。

例如,如果请求头字段太大,那么是的,当然,您可以发送一个431-请求头字段太大作为响应。但是,您强迫消费应用程序的开发人员查找431,以找出它的含义,并编写逻辑来期望该响应。相反,发送一个400坏的请求,并提供一个易于阅读和理解的有用的描述。

此外,还有一些针对特定用途的自定义代码。让我们考虑498-令牌过期和499-无效令牌。当您第一次设置身份验证时,可能会很容易使用这些。然而,经过进一步的检查,您可能会注意到,这两种方法都是专门用于ArcGIS服务器-不管是什么此外,nginx还可以使用499来指示客户端已关闭连接。

因此,在这种情况下,发送一个401-未经授权的消息,并提供一个良好的消息,为客户端提供足够的信息,以解决问题。

此外,给你的消费者一个507-不足的存储或508-循环检测,可能会给坏人更多的信息,如何使你的系统瘫痪。不要让您的消费者为您排除疑难解答。就说,嘿,抱歉-内部服务器错误。再试一次,如果你仍然有问题,这里有一个简单的方法来联系和寻求帮助。

提示3:提供正确的错误数量
根据JSONAPI规范,“服务器可能选择在遇到问题时立即停止处理,或者可能继续处理并遇到多个问题。”

他们使用“可以”的语言是有原因的。由你决定。在这一点上,你应该考虑你的客户和什么对他们有意义。首先发送一个“比萨大小是必需的”错误,然后等到下一次提交时才告诉他们,哦,顺便说一句,“皮型也是必需的”,这样做有意义吗?

在这种情况下,答案是显而易见的。但有时你可能不得不考虑这些选择。例如,如果错误仅在大量计算代价高昂的步骤之后才会发生,那么一次只返回一个错误可能是有意义的。在这两种情况下,始终要考虑使用者和将在他们一方实现的逻辑。他们想知道你早晚没有送到他们的地址吗?

那么,你一次给出一个错误的例子是什么呢?如果您收到一个由于令牌不好而导致401的请求,就没有理由继续下去,也没有理由让他们知道由于其他原因他们的请求也是不好的。他们没有被授权,所以他们不需要任何额外的信息来提示他们你的内部系统结构。

提示4:滚到最相关的错误
再次回到JSONAPI规范:“当服务器遇到单个请求的多个问题时,应该在响应中使用最适用的HTTP错误代码。例如,400-坏请求可能适用于多个4xx错误,而500-内部服务器错误可能适用于多个5xx错误。“

非常简单:总是选择最相关的广义错误,而不是更准确(但过于技术性)的特定错误。

小贴士5:解释哪里出了问题
标准接着告诉您错误消息中可能包含了什么。他们不对结果采取固执己见的态度,因为他们希望对许多用例开放。我的观点是,你应该尝试包括以下几点:

ID:一些能让你的消费者联系你的东西。然后你可以引用你的日志和度量来找出哪里出了问题。
电码即使HTTP代码返回到头部,也可以将其添加到正文中。这可以让其他人看到它,并且很容易地将它映射到一个模型中。
地位:这是您的状态代码的文本。不要让你的消费者去查。
标题:让使用者知道造成客户端错误的原因,但不要分享太多关于内部服务器错误的信息。
加分给你们中也加了以下几点的人:

链接这个是可选的,但非常有用。如果您可以链接到提供更多信息的帮助页面或自述文件,您将永远是我的英雄。但这样做的人不多,所以如果你和我们一样懒惰的话,就别觉得太糟糕了。
细部:其他信息,比如如何实际解决问题(万一你不够棒,无法提供链接)。

提示6:单独的常规错误和域错误
除了区分客户端和服务器错误以及不同的状态代码之外,我们还可以将错误分类为一般错误或特定域错误。这有助于决定是否应该编写自定义处理和消息传递,或者是众所周知的错误条件。

一般误差
一般错误包括使用错误的HTTP谓词、身份验证失败或授权失败等。通常情况下,当这些情况发生时,停止处理并向消费者发出响应是很好的。一般错误通常不是特定于域的.

域错误
这些更特定于您的域。例如,考虑是否存在DeliveryUnutiableException或OrderAlreadyExistsException。当遇到这些类型的错误时,您可能还需要做一些其他的事情。

我们应该如何实现错误处理?
好的,现在我们知道什么错误将为我们的消费者提供可靠的信息。接下来,我们该怎么做呢?我们可以做一些不同的事情,比如创建过滤器和错误处理程序。让我们来看看其中的每一个,看看它是如何完成的。

1.创建过滤器
你不想重复你自己。您希望确保所有方法都以相同的方式处理错误。这样,当您创建另一个端点时,就可以少担心一件事了。一个很好的选择是编写验证和异常过滤器,捕捉错误并以一致的方式处理它们。

验证过滤器
首先,让我们看看与验证相关的错误。在这种情况下,我们可以直接将基本验证添加到模型中,而不是检查控制器或服务中的每个字段,如下所示。

public class Pizza
{
[Require]
public string Size {get; set;}
[Require]
public string CrustType {get; set;}
public string CrustSeasoning {get; set;}
public List<Topping> Toppings {get; set;}
[DataType(DataType.Date)]
public DateTime OrderDate{get; set;}
}
然后,我们可以在ValidationActionFilter中验证我们的所有模型。

public class ValidationActionFilter: ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var modelState = actionContext.ModelState;
if (!modelState.IsValid) {
actionContext.Response = actionContext.Request
.CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
}
}
}
异常过滤器
但是我们应该如何处理其他类型的例外?我们也可以使用过滤器。

首先,让我们创建异常类。

public class PizzaParlorException: Exception
{
public PizzaParlorException(HttpStatusCode statusCode, string errorCode, string errorDescription): base($"{errorCode}::{errorDescription}")
{
StatusCode = statusCode;
}
public PizzaParlorException(HttpStatusCode statusCode)
{
StatusCode = statusCode;
}
public HttpStatusCode StatusCode {get;}
}
现在让我们创建我们的过滤器来处理我们的异常。

public class PizzaParlorExceptionFilterAttribute: ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
var exception = context.Exception as PizzaParlorException;
if (exception != null) {
context.Response = context.Request.CreateErrorResponse(
exception.StatusCode, exception.Message);
}
}
}
并将其添加到我们的GlobalConfiguration中。

GlobalConfiguration.Configuration.Filters.Add(new PizzaParlorExceptionFilterAttribute());
现在,在控制器中,我们通过扩展PizzaParlorException抛出异常,如下所示:

public class OutOfDeliveryZoneException: PizzaParlorException
{
public OutOfDeliveryZoneException(): base(HttpStatusCode.BadRequest)
{
}
}
// or perhaps the following
public class PizzaDeliveryAuthorizationException: PizzaParlorException
{
public PizzaDeliveryAuthorizationException(): base(HttpStatusCode.NotAuthorized)
{
}
}
但这不是唯一的办法处理异常。我们也可以使用GlobalErrorHandler。

2.创建GlobalExceptionHandler
首先,为什么我们要在ExceptionFilterAttribute上使用GlobalErrorHandler?有一些我们的过滤器不会捕捉到的异常。

From the ASP.NET立地,有些错误不会被我们上面的过滤器捕捉到。这包括引发的异常:

来自控制器构造函数
消息处理程序
在路由过程中
在响应内容序列化期间
由于我们也想在这些场景中进行讨论,所以让我们添加GlobalExceptionHandler。

class GlobalPizzaParlorExceptionHandler: ExceptionHandler
{
public override void HandleCore(ExceptionHandlerContext context)
{
context.Result = new TextPlainErrorResult
{
Request = context.ExceptionContext.Request,
Content = "Pizza Down! We have an Error! Please call the parlor to complete your order."
};
}
private class TextPlainErrorResult: IHttpActionResult
{
public HttpRequestMessage Request {get; set;}
public string Content {get; set;}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response =
new HttpResponseMessage(HttpStatusCode.InternalServerError);
response.Content = new StringContent(Content);
response.RequestMessage = Request;
return Task.FromResult(response);
}
}
}
现在,为了让过滤器捕获异常,我们需要做什么?将它添加到我们的GlobalConfiguration中。

public static class SetupFiltersExtensions
{
public static IAppBuilder SetupFilters(this IAppBuilder builder, HttpConfiguration config)
{
config.Services.Replace(typeof (IExceptionHandler), new PizzaParlorExceptionHandler());
return builder;
}
}
现在我们只想做一件事。

3.创建一个LoggerHandler
使用GlobalExceptionHanlder,仍然有可能不会在这里捕获异常。然而,我们保证能够记录它。我们可以根据需要设置尽可能多的异常记录器。我只是要用log4net本例中的记录器。

public class Log4NetExceptionLogger: ExceptionLogger
{
private ILog log = LogManager.GetLogger(typeof(Log4NetExceptionLogger));
public async override Task LogAsync(ExceptionLoggerContext context, System.Threading.CancellationToken cancellationToken)
{
log.Error("An unhandled exception occurred.", context.Exception);
await base.LogAsync(context, cancellationToken);
}
public override void Log(ExceptionLoggerContext context)
{
log.Error("An unhandled exception occurred.", context.Exception);
base.Log(context);
}
public override bool ShouldLog(ExceptionLoggerContext context)
{
return base.ShouldLog(context);
}
}
同样,请确保将其添加到您的配置中。

public static class WebApiConfig
{
public static IAppBuilder RegisterApiConfig(this IAppBuilder app, HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Services.Add(typeof(IExceptionLogger), new Log4NetExceptionLogger());
return app;
}
}
您还应该采取另一个步骤-使用StackifyAppender。通过这种方式,可以设置回溯以自动收集所有日志记录消息。

<log4net>
<root>
<level value="DEBUG" />
<appender-ref ref="StackifyAppender" />
</root>
<appender name="StackifyAppender" type="StackifyLib.log4net.StackifyAppender, StackifyLib.log4net" />
</log4net>
考虑你的错误处理!
今天到此为止。我们介绍了错误为使用者提供了哪些基本知识,以及如何在ASP.NET应用程序中实现错误。现在,您已经准备好在整个应用程序中提供正确的错误并一致地处理异常。

作为下一步,请查看当前项目和错误处理。验证您在正确的上下文中提供了正确的WebAPI错误,并考虑如何使您的消费者更容易自己解决问题。

猜你喜欢

转载自blog.51cto.com/14009535/2311349