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的全局錯誤處理,對於運行中錯誤的漏網之魚,要定義合適有效的全局處理器,使得不放過任何錯誤,這一點要注意。