diff --git a/core/src/main/scala/caliban/execution/Executor.scala b/core/src/main/scala/caliban/execution/Executor.scala index dd282f8e9..953c17738 100644 --- a/core/src/main/scala/caliban/execution/Executor.scala +++ b/core/src/main/scala/caliban/execution/Executor.scala @@ -313,16 +313,6 @@ object Executor { private def calculateMapCapacity(nMappings: Int): Int = Math.ceil(nMappings / 0.75d).toInt - private def effectfulExecutionError( - path: List[PathValue], - locationInfo: Option[LocationInfo], - cause: Cause[Throwable] - ): Cause[ExecutionError] = - cause.failureOption orElse cause.defects.headOption match { - case Some(e: ExecutionError) => Cause.fail(e.copy(path = path.reverse, locationInfo = locationInfo)) - case other => Cause.fail(ExecutionError("Effect failure", path.reverse, locationInfo, other)) - } - private def fieldInfo( field: Field, aliasedName: String, @@ -394,7 +384,19 @@ object Executor { wrappers match { case Nil => query case wrapper :: tail => - val q = if (isPure && !wrapper.wrapPureValues) query else wrapper.wrap(query, fieldInfo) + val q = + if (isPure && !wrapper.wrapPureValues) query + else + wrapper + .wrap(query, fieldInfo) + .mapErrorCause(e => + effectfulExecutionError( + PathValue.Key(fieldInfo.name) :: fieldInfo.path, + Some(fieldInfo.details.locationInfo), + e + ) + ) + loop(q, tail) } loop(query, fieldWrappers) @@ -508,6 +510,18 @@ object Executor { } } + private def effectfulExecutionError( + path: List[PathValue], + locationInfo: Option[LocationInfo], + cause: Cause[Throwable] + ): Cause[ExecutionError] = + cause.failureOption orElse cause.defects.headOption match { + case Some(e: ExecutionError) if e.path.isEmpty => + Cause.fail(e.copy(path = path.reverse, locationInfo = locationInfo)) + case Some(e: ExecutionError) => Cause.fail(e) + case other => Cause.fail(ExecutionError("Effect failure", path.reverse, locationInfo, other)) + } + // The implicit classes below are for methods that don't exist in Scala 2.12 so we add them as syntax methods instead private implicit class EnrichedListOps[+A](private val list: List[A]) extends AnyVal { def partitionMap[A1, A2](f: A => Either[A1, A2]): (List[A1], List[A2]) = { diff --git a/core/src/test/scala/caliban/wrappers/WrappersSpec.scala b/core/src/test/scala/caliban/wrappers/WrappersSpec.scala index 41742f8c8..1ffa5f254 100644 --- a/core/src/test/scala/caliban/wrappers/WrappersSpec.scala +++ b/core/src/test/scala/caliban/wrappers/WrappersSpec.scala @@ -8,7 +8,7 @@ import caliban.TestUtils._ import caliban.Value.{ IntValue, StringValue } import caliban.execution.{ ExecutionRequest, FieldInfo } import caliban.introspection.adt.{ __Directive, __DirectiveLocation } -import caliban.parsing.adt.{ Directive, Document } +import caliban.parsing.adt.{ Directive, Document, LocationInfo } import caliban.schema.Annotations.GQLDirective import caliban.schema.{ ArgBuilder, GenericSchema, Schema } import caliban.schema.Schema.auto._ @@ -94,6 +94,29 @@ object WrappersSpec extends ZIOSpecDefault { counter2 <- ref2.get } yield assertTrue(counter1 == 4, counter2 == 1) }, + test("Failures in FieldWrapper have a path and location") { + case class Query(a: A) + case class A(b: B) + case class B(c: Int) + + val wrapper = new FieldWrapper[Any](true) { + def wrap[R1 <: Any]( + query: ZQuery[R1, ExecutionError, ResponseValue], + info: FieldInfo + ): ZQuery[R1, ExecutionError, ResponseValue] = + if (info.name == "c") ZQuery.fail(ExecutionError("error")) + else query + } + for { + interpreter <- (graphQL(RootResolver(Query(A(B(1))))) @@ wrapper).interpreter.orDie + query = gqldoc("""{ a { b { c } } }""") + result <- interpreter.execute(query) + firstError = result.errors.head.asInstanceOf[ExecutionError] + } yield assertTrue( + firstError.path.mkString(",") == """"a","b","c"""", + firstError.locationInfo.contains(LocationInfo(11, 1)) + ) + }, test("Max fields") { case class A(b: B) case class B(c: Int)