SpringBoot 2.0.0でRouterFunctionのエラーハンドリングをWebExceptionHandlerで行う
SpringBoot 2.0.0からサポートされるRouterFunctionのエラーハンドリングをまとめていきたい。
RouterFunctionは従来のアノテーションベースでAPIを作る形ではなくDSLベースでルーティングを定義していく。
@Configuration class TaskRoutes(private val taskHandler: TaskHandler) { @Bean fun taskRouter() = router { (accept(APPLICATION_JSON) and "/api").nest { "/task".nest { POST("/", taskHandler::create) GET("/{id}", taskHandler::fetchByTaskId) PUT("/{id}", taskHandler::updateByTaskId) DELETE("/{id}", taskHandler::deleteByTaskId) PUT("/{id}/finish", taskHandler::finishByTaskId) } "/tasks".nest { GET("/", taskHandler::fetchAll) } } } }
TaskHandlerクラスは各エンドポイントを処理するが処理中に発生したエラーを共通的にハンドリングする場合にはどうすればよいか?
そんなときは WebExceptionHandler
を実装すれば良い。
@Component class ApiErrorHandler(private val objectMapper: ObjectMapper) : WebExceptionHandler { private val logger = KotlinLogging.logger {} override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> { return handle(ex) .flatMap { it.writeTo(exchange, HandlerStrategiesResponseContext(HandlerStrategies.withDefaults())) } .flatMap { Mono.empty<Void>() } } private fun handle(t: Throwable): Mono<ServerResponse> { return when (t) { is SystemException -> { "api error".let { logger.error(t) { t.message ?: it } createResponse(t.status, t.message ?: it) } } is DecodingException -> { "invalid request".let { logger.warn(t) { t.message ?: it } createResponse(HttpStatus.BAD_REQUEST, it) } } else -> { logger.error(t) { "Unknown Exception: %s".format(t.message ?: "unknown error") } createResponse(HttpStatus.INTERNAL_SERVER_ERROR, t.message ?: "internal server error") } } } private fun createResponse(httpStatus: HttpStatus, message: String, code: String? = null): Mono<ServerResponse> { return Error(objectMapper.writeValueAsString(listOf(ErrorItem(message, code, null)))).let { ServerResponse.status(httpStatus).syncBody(it) } } } private class HandlerStrategiesResponseContext(val strategies: HandlerStrategies) : ServerResponse.Context { override fun messageWriters(): List<HttpMessageWriter<*>> { return this.strategies.messageWriters() } override fun viewResolvers(): List<ViewResolver> { return this.strategies.viewResolvers() } }
[WebExceptionHandler.handle]
の引数ex
に例外クラスが渡ってくるので、メッセージとHTTPレスポンスを組み立てレスポンスBodyにセットしている。
404エラーの場合のエラーハンドリングが正しくできていることを確認。
curl -v -XGET http://localhost:8080/api/task/2 Note: Unnecessary use of -X or --request, GET is already inferred. * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > GET /api/task/2 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.56.0 > Accept: */* > < HTTP/1.1 404 Not Found < transfer-encoding: chunked < Content-Type: application/json;charset=UTF-8 < * Connection #0 to host localhost left intact {"message":"task not found","errorCode":"404","field":null}
まとめ
- これまで
FilterFunction
でエラーハンドリングを行っていたが開発をする過程で間違っていることに気づいた。ただしくWebExceptionHandlerを利用する方法をアウトプットした。合わせて Web on Reactive Stack を参照してほしい。
コード
コードはgithubにあります。
詳細なコードは次のリンクからどうぞ。 spring5-kotlin-application/ApiErrorHandler.kt at master · soushin/spring5-kotlin-application · GitHub