Sangriaにおけるエラーハンドリング

Scala 用の GraphQL ライブラリ [Sangria](https://github.com/sangria-graphql/sangria) の下記エラーハンドリングに関してです。

* (a) resolve 処理で例外が発生した場合
* (b) GraphQL クエリに問題があった場合

# はじめに

今回は以下の環境で実行しました。

##### build.sbt
```scala
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
```scala
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
```json
r1 result = {
  "data" : null,
  "errors" : [
    {
      "message" : "Internal server error",
      "path" : [
        "sample"
      ],
      "locations" : [
        {
          "line" : 1,
          "column" : 3
        }
      ]
    }
  ]
}
```

このような例外は ```ExceptionHandler``` を使ってハンドリングでき、
```HandledException``` で GraphQL 形式のエラー内容を組み立てる事ができます。

##### 処理2 - ExceptionHandler の適用例
```scala
// 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
```json
*** 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 クエリに問題がある場合
```scala
// 問題のあるクエリ（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 - クエリエラーのハンドリング例
```scala
...

r3.recover {
  case e: ErrorWithResolver => e.resolveError
}.foreach(r => println(s"r3 result2 = ${r}"))
```

##### 実行結果3
```json
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 - クエリの検証をスキップ
```scala
val r4 = Executor.execute(schema, q2, queryValidator = QueryValidator.empty)
// r4 は Future(<not completed>)
r4.foreach(r => println(s"r4 result = ${r}"))
```

##### 実行結果4
```json
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