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) }

そこで、カスタマイズしたエラーを返すようにしたいのですが、探せど探せど良い方法が見つかりません。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) }
結果

このあたりで心折れます。
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) }

ものすごく良くなりました。しかし一点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を発生させると

これにより、エラーメッセージのカスタマイズやtypeなどを表示しないなどなどエラーハンドリングが容易に出来るようになりました。
実際のコード
実際にUbieで公開している https://github.com/ubie-inc/kotlin-graphql-sample に、動くコードをコミットしてあります。上記には記述してありませんが、graphql-java-toolsのバージョン上げも必要になりますので、ご注意してください。
*1:https://graphql-java.readthedocs.io/en/latest/ が https://www.graphql-java.com/documentation にリダイレクト機能なし+ドキュメント削除して移動したことが原因です
*2:5.3で導入され、5.4.1でバグfixされています。