Skip to content

Latest commit

 

History

History
180 lines (142 loc) · 7.69 KB

Vert.x - Web API Contract 错误处理.md

File metadata and controls

180 lines (142 loc) · 7.69 KB
created_at updated_at slug tags
2020-03-14 11:43:43 -0700
2021-02-16 15:22:42 -0800
vertx-web-api-contract-error-handle
Vert.x
错误处理

Vert.x Web API Contract模块在Vert.x Web的基础上进行扩展,支持OpenAPI 3.0规范。

使用上有两种方式

  • 编程方式

    预定义HTTPRequestValidationHandler,并在route中传入,就像手册给的那样

  • 配置文件方式

    预先定义好接口描述文件,通过OpenAPI3RouterFactory加载并挂载到Router上

    val openAPI3RouterFactoryList = mutableListOf<OpenAPI3RouterFactory>()
    listOf(
        "/webroot/swagger/openapi-admin.yaml"
    ).forEach { configPath ->
               awaitResult<OpenAPI3RouterFactory> {
                   OpenAPI3RouterFactory.create(vertx, configPath, it)
               }.apply {
                   openAPI3RouterFactoryList.add(this)
               }
    }
    val mainRouter = Router.router(vertx)
    openAPI3RouterFactoryList.forEach { routerFactory ->
          mainRouter.mountSubRouter("/", routerFactory.mountServicesFromExtensions()..router)
    }

    通过这种方式加载,Vert.x能够自动解析描述文件,提供自动挂载验证handler和securityHandler的能力,在请求不符合配置文件定义的约束时,能够自动以合适的状态码回绝用户请求。

问题

使用配置文件方式生成Route,不增加额外Handler的情况下,在遇到验证失败或空指针异常之类的情况时,Vert.x处理验证错误和内部错误的方式是直接报400 Bad Request和500 Internal Error,没有任何附加信息,日志上也不会有任何输出,这为问题排查和API使用者都是很不友好的方式。

image-20200314115204080

解决方案

通过查看手册和源码跟踪,找到如下处理方式,在Router上挂载一个全局的400和500错误处理器,对错误信息进行详细解析

val OpenAPIErrorTypeMap = mapOf(
  Pair(ValidationException.ErrorType.NO_MATCH, "格式错误"),
  Pair(ValidationException.ErrorType.NOT_FOUND, "缺失"),
  Pair(ValidationException.ErrorType.UNEXPECTED_ARRAY, "不应为数组"),
  Pair(ValidationException.ErrorType.UNEXPECTED_SINGLE_STRING, "必须为数组"),
  Pair(ValidationException.ErrorType.FILE_NOT_FOUND, "文件未找到"),
  Pair(ValidationException.ErrorType.WRONG_CONTENT_TYPE, "请求头Content-Type错误"),
  Pair(ValidationException.ErrorType.EMPTY_VALUE, "值不应为空"),
  Pair(ValidationException.ErrorType.UNEXPECTED_ARRAY_SIZE, "数组容量不匹配"),
  Pair(ValidationException.ErrorType.DESERIALIZATION_ERROR, "反序列化失败"),
  Pair(ValidationException.ErrorType.OBJECT_FIELD_NOT_FOUND, "缺失"),
  Pair(ValidationException.ErrorType.JSON_NOT_PARSABLE, "JSON无法解析"),
  Pair(ValidationException.ErrorType.JSON_INVALID, "格式错误"),
  Pair(ValidationException.ErrorType.XML_INVALID, "格式错误")
)

// 针对OpenAPI的参数校验错误
mainRouter.errorHandler(400) {
    if (it.failure() is ValidationException) {
        val failure = it.failure() as ValidationException
        val msg = JsonObject()
        .put("errCode", "BadRequest")
        .put("errMsg", it.failure().message)
        .put("userMsg", "请求参数 ${failure.parameterName()} ${OpenAPIErrorTypeMap[failure.type()]}")
        it.response()
        .setStatusCode(it.statusCode())
        .putHeader("Content-type", "application/json")
        .putHeader("Content-length", "${msg.toString().toByteArray().size}")
        .write(msg.toBuffer()).end()
    }
}

如此,若再发生请求参数验证错误,将给出明确的问题所在,而不是靠猜测。

image-20200314115352490

方案出处

遇到这个问题时,我个人倾向于Vert.x应该会提供一个打印详细报错信息的开关之类的东西,但在手册中并没有找到,于是通过源码定位到如下报错地点。

io.vertx.ext.web.api.validation.impl.BaseValidationHandler#handle()

@Override
  public void handle(RoutingContext routingContext) {
    try {
      RequestParametersImpl parsedParameters = new RequestParametersImpl();

      parsedParameters.setPathParameters(validatePathParams(routingContext));
      parsedParameters.setQueryParameters(validateQueryParams(routingContext));
      parsedParameters.setHeaderParameters(validateHeaderParams(routingContext));
      parsedParameters.setCookieParameters(validateCookieParams(routingContext));
      
      // . . . . . .
      
      routingContext.next();
    } catch (ValidationException e) {
      routingContext.fail(400, e);
    }
  }

可以看到,在验证失败时,它直接将routingContext设置为了400错误,并将异常一并传入,查看routingContext.fail()方法定义,明确说明,如果没有任何错误处理器对该状态码进行处理,将直接向客户端响应状态码对应的默认响应,对于400的默认响应,就是statucode=400和statusmessage=Bad Request

  /**
   * Fail the context with the specified throwable and the specified the status code.
   * <p>
   * This will cause the router to route the context to any matching failure handlers for the request. If no failure handlers
   * match It will trigger the error handler matching the status code. You can define such error handler with
   * {@link Router#errorHandler(int, Handler)}. If no error handler is not defined, It will send a default failure response with provided status code.
   *
   * @param statusCode the HTTP status code
   * @param throwable a throwable representing the failure
   */
  void fail(int statusCode, Throwable throwable);

因此,就此处来讲,定义针对400的处理器是非常有必要的,同时也是官方推荐的处理方式,官方文档中特意提到了错误处理的管理方式,有如下两种,很明显,对于很多path的情况,使用第二种更好。

  • 单独为一个路径增加错误处理器

    router.get("/awesome/:pathParam")
      // Mount validation handler
      .handler(validationHandler)
      //Mount your handler
      .handler((routingContext) -> {
        // Your logic
      })
      //Mount your failure handler to manage the validation failure at path level
      .failureHandler((routingContext) -> {
        Throwable failure = routingContext.failure();
        if (failure instanceof ValidationException) {
          // Something went wrong during validation!
          String validationErrorMessage = failure.getMessage();
        }
      });
  • 为一个状态码增加错误处理器

    // Manage the validation failure for all routes in the router
    router.errorHandler(400, routingContext -> {
      if (routingContext.failure() instanceof ValidationException) {
        // Something went wrong during validation!
        String validationErrorMessage = routingContext.failure().getMessage();
      } else {
        // Unknown 400 failure happened
        routingContext.response().setStatusCode(400).end();
      }
    });

延伸

关于在Router上为某个特定的状态码增加错误处理器的处理,不仅在于此处,个人认为是可以通用的,对高频发生的状态码,可以这样增加一个全局处理器,使得不至于丢失错误信息。

此外,这也衍生出另一个问题,Vert.x的全局错误处理,对于运行中错误的漏网之鱼,要定义合适有效的全局处理器,使得不放过任何错误,这一点要注意。

参考文档

  1. Vert.x Web API Contract

  2. Swagger OpenAPI Specification