すきま風

勉強したことのメモとか

Spring WebFlux × Kotlinで NoHandlerFoundException 対策をする

背景

Spring BootでRest APIを作る際、Spring MVCならNoHandlerFoundExceptionがあるため、設定していないURLにRequestが来た場合のハンドリングは ExceptionHandlerで簡単に実装できる。

@RestControllerAdvice
class ErrorAdvice {
    @ExceptionHandler(value = [NoHandlerFoundException::class])
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun noHandleFound(ex: NoHandlerFoundException, req: WebRequest): ErrorResponse {
        return ErrorResponse("no handler found")
    }
}

しかし、WebFluxでは前述のNoHandlerFoundExceptionが存在しないので、ExceptionHandlerでは実装できない。なので、自分で実装するしかない、多分 (自信なし)。

実装

AbstractErrorWebExceptionHandler を継承したClassを実装する。

ライブラリ

Library Version
Kotlin 1.6.0
Spring Boot 2.6.0
@Component
@Order(-2)
class MyExceptionHandler(
    errorAttributes: ErrorAttributes,
    applicationContext: ApplicationContext,
    serverCodecConfigurer: ServerCodecConfigurer,
) : AbstractErrorWebExceptionHandler(
    errorAttributes,
    WebProperties.Resources(),
    applicationContext
) {
    init {
        // https://spring.pleiades.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-config-message-codecs
        // defaultを設定
        this.setMessageWriters(serverCodecConfigurer.writers)
        this.setMessageReaders(serverCodecConfigurer.readers)
    }

    override fun getRoutingFunction(errorAttributes: ErrorAttributes?): RouterFunction<ServerResponse> {
        return route(all(), this::renderErrorResponse)
    }

    private fun renderErrorResponse(serverRequest: ServerRequest): Mono<ServerResponse> {
        val errorProperties = getErrorAttributes(
            serverRequest,
            ErrorAttributeOptions.defaults()
        )

        logger.debug { errorProperties }

        val errorAttribute = serverRequest
            .attribute(
                "org.springframework.boot.web.reactive.error.DefaultErrorAttributes.ERROR"
            )

        if (errorAttribute.isPresent) {
            val th = errorAttribute.get()

            if (th is Throwable && th.message == NO_HANDLER_MESSAGE) {
                return ServerResponse.status(HttpStatus.BAD_REQUEST)
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(
                        BodyInserters.fromValue(ErrorResponse(NO_HANDLER_MESSAGE))
                    )
            }
        }

        return ServerResponse.status(HttpStatus.BAD_REQUEST)
            .contentType(MediaType.APPLICATION_JSON)
            .body(BodyInserters.fromValue(ErrorResponse("something wrong ;-(")))
    }

    companion object {
        private const val NO_HANDLER_MESSAGE = "404 NOT_FOUND \"No matching handler\""
        private val logger = KotlinLogging.logger { }
    }
}

data class ErrorResponse(
    val reason: String
)

あとはapplication.ymlでwhitelabelを無効にしておく。

spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false

server:
  error:
    whitelabel:
      enabled: false

一応動いているから、多分大丈夫、大丈夫だよな。。KotlinでのAbstractErrorWebExceptionHandlerがいくらググっても全然出てこないから若干ゃ不安

参考

Spring Boot Reference Documentation

spring - String insted of whitelabel error page in webflux? - Stack Overflow