graphql-spring-boot-starterのExceptionハンドリングがめっちゃ便利になってた

Kotlinサーバサイド開発者のみなさん、graphql-javaでの開発の調子はどうでしょうか?

去年末にリリースされたバージョンのgraphql-spring-boot-starterのExceptionハンドリングがめっちゃ便利で感動したので、ブログに残しておきます。

想定読者

  • graphql-spring-boot-starter使って、GraphQLやってる人/これからやる人

はじめに

graphql-spring-boot-starterのバージョンが5.0.4の時、Resolver内で発生したExceptionは特に何も指定しない場合、全て Internal Server Error(s) while executing query と言うmessageを返していました。 例えば、AuthException のような自前で作成したExceptionが出た場合、レスポンスとしては違ったものを返したくなりますが、問答無用でInternal Server Error扱いです。(ちなみに最新版でも互換性を残すため、同じ挙動です)

    @Suppress("unused")
    fun diseases(icd: String): List<Disease> {
        if (icd.isEmpty()) throw IllegalArgumentException("icd must not be empty")
        return service.getDiseases(icd)
    }

f:id:shiraji:20190424022910p:plain
何だろうとInternal Server Error!

そこで、カスタマイズしたエラーを返すようにしたいのですが、探せど探せど良い方法が見つかりません。issueにこのドキュメント通りに書けばいけるぜ!と書かれているので、URLをクリックすると

        \          SORRY            /
         \                         /
          \    This page does     /
           ]   not exist yet.    [    ,'|
           ]                     [   /  |
           ]___               ___[ ,'   |
           ]  ]\             /[  [ |:   |
           ]  ] \           / [  [ |:   |
           ]  ]  ]         [  [  [ |:   |
           ]  ]  ]__     __[  [  [ |:   |
           ]  ]  ] ]\ _ /[ [  [  [ |:   |
           ]  ]  ] ] (#) [ [  [  [ :===='
           ]  ]  ]_].nHn.[_[  [  [
           ]  ]  ]  HHHHH. [  [  [
           ]  ] /   `HH("N  \ [  [
           ]__]/     HHH  "  \[__[
           ]         NNN         [
           ]         N/"         [
           ]         N H         [
          /          N            \
         /           q,            \
        /                           \

こんなページに飛ばされ、かなりイライラします。*1

でまぁ、色々調べていくと、Authエラーなどで全処理中断させたいであれば、 GraphQLException を実装すれば、一般的なランタイム時のExceptionとなり、エラーを返してくれると書いてあるので、試してみます。

    @Suppress("unused")
    fun diseases(icd: String): List<Disease> {
        if (icd.isEmpty()) throw GraphQLException("icd must not be empty")
        return service.getDiseases(icd)
    }

結果

f:id:shiraji:20190424023144p:plain
返してくれなかったよ・・・涙

このあたりで心折れます。

Spring BootでのExceptionハンドリング

話変わって、Spring Bootには結構便利なExceptionハンドラがあります。 以下のような感じで、各種Controller内で発生したExceptionに対して、一箇所でうまくシュッとやってくれます 😤

@RestControllerAdvice(basePackageClasses = [ExceptionHandlerAdvice::class])
class MyExceptionHandlerAdvice {

    @ExceptionHandler(TimeoutException::class)
    @ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
    fun handle(exception: TimeoutException): ExceptionResource {
        log.error("caught an exception", exception)
        return ExceptionResource("タイムアウト")
    }

    @ExceptionHandler(WebExchangeBindException::class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun handle(exception: TimeoutException): ExceptionResource {
        log.error("caught an exception", exception)
        return ExceptionResource("なんか起こった")
    }

}

まぁあんまり詳しく書いても話が逸れるので、こんな感じのものがあるのだなー程度で良いです。

で、graphql-javaでもこんな感じで処理したい!と思う訳です。

時間が解決してくれた(=中の人達が頑張ってくれた)

それから数ヶ月が経ち、graphql-spring-boot-starterがメキメキ更新されていました。

現在の最新のバージョン(5.7.3)を使い、graphql.servlet.exception-handlers-enabledをtrueにすると以下のコードでもうまくハンドリングしてくれるようになります。*2

    @Suppress("unused")
    fun diseases(icd: String): List<Disease> {
        if (icd.isEmpty()) throw IllegalArgumentException("icd must not be empty")
        return service.getDiseases(icd)
    }

f:id:shiraji:20190424030032p:plain
IllegalArgumentExceptionですらうまくメッセージを返すように

ものすごく良くなりました。しかし一点typeがException名になってしまいます。 フロント側で IllegalArgumentException を処理してもらうで良いのですが、ちょっとダサい。GraphQLExceptionをthrowしたとしても、同じようにtypeが表示されてしまいます。

Spring BootのExceptionハンドリングのように特定のExceptionに対して、カスタマイズしたGraphQLErrorを表示したくなります。

そこで登場したのが、exception-handlers-enabledが導入されたタイミングでgraphql-spring-boot-starterが @ExceptionHandler によるExceptionのカスタマイズ機能です。実装方法としてはこんな感じのBeanやComponentをクラスを作るだけ!

@Component
class GraphQLExceptionHandler {
    @ExceptionHandler(IllegalArgumentException::class)
    fun handleSomeException(e: Throwable): GraphQLError {
        return GenericGraphQLError("Foo! ${e.message}")
    }
}

それで同じようにExceptionを発生させると

f:id:shiraji:20190424031759p:plain
メッセージやtypeを表示しないなどのカスタマイズが出来た!

これにより、エラーメッセージのカスタマイズやtypeなどを表示しないなどなどエラーハンドリングが容易に出来るようになりました。

実際のコード

実際にUbieで公開している https://github.com/ubie-inc/kotlin-graphql-sample に、動くコードをコミットしてあります。上記には記述してありませんが、graphql-java-toolsのバージョン上げも必要になりますので、ご注意してください。

github.com

*1:https://graphql-java.readthedocs.io/en/latest/https://www.graphql-java.com/documentation にリダイレクト機能なし+ドキュメント削除して移動したことが原因です

*2:5.3で導入され、5.4.1でバグfixされています。