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されています。