Scala 用の GraphQL ライブラリ Sangria の下記エラーハンドリングに関してです。
- (a) resolve 処理で例外が発生した場合
- (b) GraphQL クエリに問題があった場合
はじめに
今回は以下の環境で実行しました。
build.sbt
ThisBuild / scalaVersion := "3.2.1"
libraryDependencies ++= Seq(
"org.sangria-graphql" %% "sangria" % "3.4.1",
"org.sangria-graphql" %% "sangria-circe" % "1.3.2"
)
GraphQL の結果を JSON 文字列で出力するため sangria-circe
を使っています。
(a) resolve 処理で例外が発生した場合
resolve 処理で何らかの例外が発生した場合、デフォルトでエラーメッセージが Internal server error となります。
例えば、以下のような処理を実行した場合
処理1
import sangria.schema.*
import sangria.execution.*
import sangria.macros.graphql
import sangria.marshalling.circe.*
import scala.concurrent.ExecutionContext
...
val QueryType = ObjectType("Query", fields[Unit, Unit](
Field(
"sample",
StringType,
arguments = Argument("input", StringType) :: Nil,
resolve = c =>
val input = c.args.arg[String]("input")
if input.isBlank then
throw Exception("input is blank")
else
s"ok-${input}"
)
))
val schema = Schema(QueryType)
val q = graphql"""{ sample(input: "") }"""
given ExecutionContext = ExecutionContext.global
val r1 = Executor.execute(schema, q)
// r1 は Future(<not completed>)
r1.foreach(r => println(s"r1 result = ${r}"))
実行結果は次のようになります。
Future
としては成功扱いとなる点に注意。
実行結果1
r1 result = {
"data" : null,
"errors" : [
{
"message" : "Internal server error",
"path" : [
"sample"
],
"locations" : [
{
"line" : 1,
"column" : 3
}
]
}
]
}
このような例外は ExceptionHandler
を使ってハンドリングでき、
HandledException
で GraphQL 形式のエラー内容を組み立てる事ができます。
処理2 - ExceptionHandler の適用例
// ExceptionHandler の実装
val exceptionHandler = ExceptionHandler {
case (_, e) => {
println(s"*** Error Handling: ${e}")
// エラーメッセージを変更
HandledException(e.getMessage)
}
}
val r2 = Executor.execute(schema, q, exceptionHandler = exceptionHandler)
r2.foreach(r => println(s"r2 result = ${r}"))
ExceptionHandler の適用結果は次のようになりました。
実行結果2
*** Error Handling: java.lang.Exception: input is blank
r2 result = {
"data" : null,
"errors" : [
{
"message" : "input is blank",
"path" : [
"sample"
],
"locations" : [
{
"line" : 1,
"column" : 3
}
]
}
]
}
(b) GraphQL クエリに問題があった場合
次に、以下のように GraphQL クエリ自体に問題があった場合、ExceptionHandler ではハンドリングされず、Future(Failure(sangria.execution.ValidationError: Query does not pass validation. Violations: ・・・
のように Future 自体が失敗扱いとなります。
GraphQL クエリに問題がある場合
// 問題のあるクエリ(input 引数が未指定)
val q2 = graphql"{ sample }"
val r3 = Executor.execute(schema, q2, exceptionHandler = exceptionHandler)
// r3 が Future(Failure(sangria.execution.ValidationError)) となり foreach の処理は実行されない
r3.foreach(r => println(s"r3 result1 = ${r}"))
そのため、Future の recover
でエラーハンドリングするしか無さそうです。
エラー自体は ValidationError
の他にもいくつか用意されているようですが、どのエラーも基本的に ErrorWithResolver
トレイトを実装しており、その resolveError
メソッドを使う事で GraphQL 形式のエラー結果 {・・・, "errors": [・・・]}
を得られます。
処理3 - クエリエラーのハンドリング例
...
r3.recover {
case e: ErrorWithResolver => e.resolveError
}.foreach(r => println(s"r3 result2 = ${r}"))
実行結果3
r3 result2 = {
"data" : null,
"errors" : [
{
"message" : "Field 'sample' argument 'input' of type 'String!' is required but not provided. (line 1, column 3):\n{ sample }\n ^",
"locations" : [
{
"line" : 1,
"column" : 3
}
]
}
]
}
また、queryValidator を設定してクエリの検証をスキップする事も可能でしたが、ExceptionHandler ではハンドリングできないようですし、基本的には使わない方が良さそうです。
処理4 - クエリの検証をスキップ
val r4 = Executor.execute(schema, q2, queryValidator = QueryValidator.empty)
// r4 は Future(<not completed>)
r4.foreach(r => println(s"r4 result = ${r}"))
実行結果4
r4 result = {
"data" : null,
"errors" : [
{
"message" : "Null value was provided for the NotNull Type 'String!' at path 'input'. (line 1, column 3):\n{ sample }\n ^",
"path" : [
"sample"
],
"locations" : [
{
"line" : 1,
"column" : 3
}
]
}
]
}
今回のサンプルコードは https://github.com/fits/try_samples/tree/master/blog/20230107/sangria_error_handling