SpringBoot 2.0.0でRouterFunctionのエラーハンドリングをWebExceptionHandlerで行う


SpringBoot 2.0.0からサポートされるRouterFunctionのエラーハンドリングをまとめていきたい。

RouterFunctionは従来のアノテーションベースでAPIを作る形ではなくDSLベースでルーティングを定義していく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@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を実装すれば良い。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@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エラーの場合のエラーハンドリングが正しくできていることを確認。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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}

まとめ

コード

コードはgithubにあります。

詳細なコードは次のリンクからどうぞ。 spring5-kotlin-application/ApiErrorHandler.kt at master · soushin/spring5-kotlin-application · GitHub