资讯专栏INFORMATION COLUMN

Restful API 中的错误处理

NotFound / 1255人阅读

简介

随着移动开发和前端开发的崛起,越来越多的 Web 后端应用都倾向于实现 Restful API。
Restful API 是一个简单易用的前后端分离方案,它只需要对客户端请求进行处理,然后返回结果即可, 无需考虑页面渲染,一定程度上减轻了后端开发人员的负担。
然而,正是由于 Restful API 不需要考虑页面渲染,导致它不能在页面上展示错误信息。
那就意着当出现错误的时候,它只能通过返回一个错误的响应,来告诉用户和开发者相应的错误信息,提示他们接下来应该怎么办。
本文将讨论 Restful API 中的错误处理方案。

设计错误信息

当 Restful API 需要抛出错误的时候,我们要考虑的是:这个错误应该包含哪些信息。
我们先看看 Github, Google, Facebook, Twitter, Twilio 的错误信息是怎样的。

Github (use http status)

{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "Issue",
      "field": "title",
      "code": "missing_field"
    }
  ]
}

Google (use http status)

{
  "error": {
    "errors": [
      {
        "domain": "global",
        "reason": "insufficientFilePermissions",
        "message": "The user does not have sufficient permissions for file {fileId}."
      }
    ],
    "code": 403,
    "message": "The user does not have sufficient permissions for file {fileId}."
  }
}

Facebook (use http status)

{
  "error": {
    "message": "Message describing the error", 
    "type": "OAuthException",
    "code": 190,
    "error_subcode": 460,
    "error_user_title": "A title",
    "error_user_msg": "A message",
    "fbtrace_id": "EJplcsCHuLu"
  }
}

Twitter (use http status)

{
  "errors": [
    {
      "message": "Sorry, that page does not exist",
      "code": 34
    }
  ]
}

Twilio (use http status)

{
  "code": 21211,
  "message": "The "To" number 5551234567 is not a valid phone number.",
  "more_info": "https://www.twilio.com/docs/errors/21211",
  "status": 400
}

观察这些结构可以发现它们都有一些共同的地方:

都利用了 Http 状态码

有些返回了业务错误码

都提供了给用户看的错误提示信息

有些提供了给开发者看的错误信息

Http 状态码

在 Restful API 中利用 Http 状态码来表明错误类型再合适不过了,因为 Http 状态码定义了很多抽象的错误类型。
虽然 Http 状态码定义了非常多的错误类型,但实际应用中,我们常用的状态码并不多,通常都是下面这几方面:

API 正常工作 (200, 201)

客户端错误 (400, 401, 403, 404)

服务端错误 (500, 503)

业务错误码

很多时候,我们根据业务类型来自定义错误码。
这些业务错误码与 Http 状态码并不重叠,这时候我们可以返回业务错误码,用来提示用户/开发者错误类型。

给用户看的错误信息

当出现错误的时候,我们需要提示用户如何处理这种情况,通常这种错误信息都是必须的。
可以看到上面几个例子中都有返回给用户看的错误信息。

给开发者看的错误信息

若我们的 API 需要开放给第三方开发者,那么我们就需要考虑返回一些给开发者看的错误信息。

设计错误类型

我们刚才提到过,可以利用 Http 状态码来为错误类型进行分类。
通常我们所说的分类通常是对客户端错误进行分类, 即 4xx 类型的错误。

而这些错误类型中,我们最常用的是:

400 Bad Request
由于包含语法错误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求。
通常在请求参数不合法或格式错误的时候可以返回这个状态码。

401 Unauthorized
当前请求需要用户验证。
通常在没有登录的状态下访问一些受保护的 API 时会用到这个状态码。

403 Forbidden
服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助。
通常在没有权限操作资源时(如修改/删除一个不属于该用户的资源时)会用到这个状态码。

404 Not Found
请求失败,请求所希望得到的资源未被在服务器上发现。
通常在找不到资源时返回这个状态码。

尽管我们可以通过 Http 状态码来表示错误的类型,
但在实际应用中,如果仅仅使用 Http 状态码的话,我们的代码中就遍布 Http 状态码:

// Node.js
if (!res.body.title) {
  res.statusCode = 400
}

if (!user) {
  res.statusCode = 401
}

if (!post) {
  res.statusCode = 404
}

上面的实现方式在小项目中还可以接受,当项目变大、需求变多的时候,维护起来就变得很麻烦了。
为了提高错误的可读性和可维护性,我们需要对各种错误进行分类。
我个人习惯把错误分成以下几种类型:

格式错误 (FORMAT_INVALID)

数据不存在 (DATA_NOT_FOUND)

数据已存在 (DATA_EXISTED)

数据无效 (DATA_INVALID)

登录错误 (LOGIN_REQUIRED)

权限不足 (PERMISSION_DENIED)

错误分类之后,我们抛错误的时候就变得更加直观了:

if (!res.body.title) {
  throw new Error(ERROR.FORMAT_INVALID)
}

if (!user) {
  throw new Error(ERROR.LOGIN_REQUIRED)
}

if (!post) {
  throw new Error(ERROR.DATA_NOT_FOUND)
}

if (post.creator.id !== user.id) {
  throw new Error(ERROR.PERMISSION_DENIED)
}

这种形式比上面的写死状态码的方式方便很多,而且维护起来也更加简单。
但有一个问题,就是不能根据错误类型来返回指定的错误信息。

自定义错误类型

要实现根据错误类型来返回指定的错误信息,我们可以通过自定义错误的方式来实现。
假设我们自定义错误的结构如下:

{
  "type": "",
  "code": 0,
  "message": "",
  "detail": ""
}

我们需要做到如下几点:

根据错误类型来自动设置 type, code, message

detail 为可选项,用来描述该错误的具体原因

const ERROR = {
  FORMAT_INVALID: "FORMAT_INVALID",
  DATA_NOT_FOUND: "DATA_NOT_FOUND",
  DATA_EXISTED: "DATA_EXISTED",
  DATA_INVALID: "DATA_INVALID",
  LOGIN_REQUIRED: "LOGIN_REQUIRED",
  PERMISSION_DENIED: "PERMISSION_DENIED"
}

const ERROR_MAP = {
  FORMAT_INVALID: {
    code: 1,
    message: "The request format is invalid"
  },
  DATA_NOT_FOUND: {
    code: 2,
    message: "The data is not found in database"
  },
  DATA_EXISTED: {
    code: 3,
    message: "The data has exist in database"
  },
  DATA_INVALID: {
    code: 4,
    message: "The data is invalid"
  },
  LOGIN_REQUIRED: {
    code 5,
    message: "Please login first"
  },
  PERMISSION_DENIED: {
    code: 6,
    message: "You have no permission to operate"
  }
}

class CError extends Error {
  constructor(type, detail) {
    super()
    Error.captureStackTrace(this, this.constructor)

    let error = ERROR_MAP[type]
    if (!error) {
      error = {
        code: 999,
        message: "Unknow error type"
      }
    }

    this.name = "CError"
    this.type = error.code !== 999 ? type : "UNDEFINED"
    this.code = error.code
    this.message = error.message
    this.detail = detail
  }
}

自定义好错误之后,我们调用起来就更加简单了:

// in controller
if (!user) {
  throw new CError(ERROR.LOGIN_REQUIRED, "You should login first")
}

if (!req.body.title) {
  throw new CError(ERROR.FORMAT_INVALID, "Title is required")
}

if (!post) {
  throw new CError(ERROR.DATA_NOT_FOUND, "The post you required is not found")
}

最后,还剩下一个问题,根据错误类型来设置状态码,然后返回错误信息给客户端。

捕获错误信息

在 Controller 中抛出自定义错误后,我们需要捕获该错误,才能返回给客户端。
假设我们使用 koa 2 作为 web 框架来开发 restful api,那么我们要做的是添加错误处理的中间件:

module.exports = async function errorHandler (ctx, next) {
  try {
    await next()
  } catch (err) {

    let status

    switch (err.type) {
      case ERROR.FORMAT_INVALID:
      case ERROR.DATA_EXISTED:
      case ERROR.DATA_INVALID:
        status = 400
        break
      case ERROR.LOGIN_REQUIRED:
        status = 401
      case ERROR.PERMISSION_DENIED:
        status = 403
      case ERROR.DATA_NOT_FOUND:
        status = 404
        break
      default:
        status = 500
    }

    ctx.status = status
    ctx.body = err
  }
}

// in app.js
app.use(errorHandler)
app.use(router.routes())

通过这种方式,我们就能优雅地处理 Restful API 中的错误信息了。

参考资料

https://zh.wikipedia.org/zh-hans/HTTP%E7%8A%B6%E6%80%81%E7%A0%81
https://www.loggly.com/blog/n...
http://blog.restcase.com/rest...
https://apigee.com/about/blg/...
http://stackoverflow.com/ques...
http://goldbergyoni.com/check...
http://blogs.mulesoft.com/dev...
https://developers.facebook.c...
https://developers.google.com...
https://developer.github.com/...
https://dev.twitter.com/overv...
https://www.twilio.com/docs/a...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/75712.html

相关文章

  • 7点关于RESTful规范的API接口设计的想法

    摘要:返回值结构在完成了上面的部署之后,接下来我们来看看返回结果应该怎么样来确定。因为返回值中,我们常常要对数据进行区分分组,或者按照从属关系打包,所以,我们再返回时,最好有包裹的思想,把数据存放在不同的包裹中进行返回。 在项目中,需要为APP撰写API。刚开始接触的时候,并没有考虑太多,就想提供URL,APP端通过该URL进行查询、创建、更新等操作即可。但再对相关规范进行了解后,才发现,A...

    Jason 评论0 收藏0
  • 使用swagger 生成 Flask RESTful API

    摘要:指定筛选条件选择合适的状态码应答中,需要带一个很重要的字段。返回结果针对不同操作,服务器向用户返回的结果应该符合以下规范。如果状态码是,就应该向用户返回出错信息。 什么是 RESTful 什么是REST REST(英文:Representational State Transfer,又称具象状态传输)是Roy Thomas Fielding博士于2000年在他的博士论文 中提出来的一种...

    printempw 评论0 收藏0
  • Flask 扩展系列之 Flask-RESTful

    摘要:励以最少的安装方式进行最佳实践。上面的例子接收了一个对象并准备将其序列化。装饰器会通过进行转换。从对象中提取的唯一字段是。是一个特殊的字段,它接受端点名称并为响应中的端点生成一个。可以查看项查看完整列表。 大纲 简介 安装 快速入门 一个最小的 api 例子 资源丰富的路由 端点 参数解析 数据格式化 完整 TODO 应用例子 简介 Flask-RESTful是一个Flas...

    阿罗 评论0 收藏0
  • SpringBoot RESTful 应用中的异常处理小结

    摘要:和的区别方法注解作用于级别注解为一个定义一个异常处理器类注解作用于整个工程注解定义了一个全局的异常处理器需要注意的是的优先级比高即抛出的异常如果既可以让标注的方法处理又可以让标注的类中的方法处理则优先让标注的方法处理处理中的异常为了方便地展 @ControllerAdvice 和 @ExceptionHandler 的区别 ExceptionHandler, 方法注解, 作用于 Co...

    jackzou 评论0 收藏0
  • 人人都是 API 设计师:我对 RESTful API、GraphQL、RPC API 的思考

    摘要:通常情况下,伪都是基于第一层次与第二层次设计的。为了解决这个版本不兼容问题,在设计的一种实用的做法是使用版本号。例如,建议第三位版本号通常表示兼容升级,只有不兼容时才需要变更服务版本。 原文地址:梁桂钊的博客 博客地址:blog.720ui.com 欢迎关注公众号:「服务端思维」。一群同频者,一起成长,一起精进,打破认知的局限性。 有一段时间没怎么写文章了,今天提笔写一篇自己对 API 设...

    ormsf 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<