Vert.x - 接管Web API Contract的驗證錯誤(問題記錄)

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使用者都是很不友好的方式。
在這裏插入圖片描述

解決方案

通過查看手冊和源碼跟蹤,找到如下處理方式,在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()
    }
}

如此,若再發生請求參數驗證錯誤,將給出明確的問題所在,而不是靠猜測。
在這裏插入圖片描述

方案出處

遇到這個問題時,我個人傾向於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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章