aPureBase / KGraphQL

Pure Kotlin GraphQL implementation
https://kgraphql.io
MIT License
298 stars 58 forks source link

Add extensions field to error response #171

Open ima9dan opened 2 years ago

ima9dan commented 2 years ago

Change 1: Added extensions to the error response. Change 2: Added debug option to GraphQL Configuration (flag to output exception information to extensions) Change 3: Moved serialize (), which was defined as an extension of GraphQLError, into GraphQLError. option


Change 1: Added extensions to the error response.

fields in extensions.

ex1:

throw GraphQLError("validation error!","VALIDATION_ERROR",
    mapOf<String,Any?>("singleCheck" to mapOf<String,String>("email" to "not an email", "age" to "Limited to 150"), 
                        "multiCheck" to "The 'from' number must not exceed the 'to' number"))

output

{
  "errors": [
    {
      "message": "validation error!",
      "locations": [],
      "path": [],
      "extensions": {
        "type": "VALIDATION_ERROR",
        "detail": {
          "singleCheck": {
            "email": "not an email",
            "age": "Limited to 150"
          },
          "multiCheck": "The 'from' number must not exceed the 'to' number"
        }
      }
    }
  ]
}

ex2:

throw GraphQLError("Access Token has expired","AUTHORIZATION_ERROR")

output

{
  "errors": [
    {
      "message": "Access Token has expired",
      "locations": [],
      "path": [],
      "extensions": {
        "type": "AUTHORIZATION_ERROR",
      }
    }
  ]
}

Change 2: Added debug option to GraphQL Configuration (flag to output exception information to extensions)

First, set the debug option to true when installing GraphQL

install(GraphQL) {
    useDefaultPrettyPrinter = true
    playground = true
    debug = true   // added option
    endpoint = "/"

    wrap {
        authenticate(optional = true, build = it)
    }
    context { call ->
        call.authentication.principal<User>()?.let {
            +it
        }
    }
    schema { ufoSchema() }
}

Then raise GraphQLError

throw GraphQLError("Access Token has expired","AUTHORIZATION_ERROR")

Finaly, the debug field is automatically added to extensions, and the detailed information of exception is output.

{
  "errors": [
    {
      "message": "Access Token has expired",
      "locations": [],
      "path": [],
      "extensions": {
        "type": "AUTHORIZATION_ERROR",
        "debug": {
          "fileName": "GraphQLSchema.kt",
          "line": "38",
          "method": "invokeSuspend",
          "classPath": "com.apurebase.kgraphql.GraphQLSchemaKt$ufoSchema$3$1",
          "stackTrace": [
            "com.apurebase.kgraphql.GraphQLSchemaKt$ufoSchema$3$1.invokeSuspend(GraphQLSchema.kt:38)",
            "com.apurebase.kgraphql.GraphQLSchemaKt$ufoSchema$3$1.invoke(GraphQLSchema.kt)",
            "com.apurebase.kgraphql.GraphQLSchemaKt$ufoSchema$3$1.invoke(GraphQLSchema.kt)",
            "com.apurebase.kgraphql.schema.model.FunctionWrapper$ArityZero.invoke(FunctionWrapper.kt:159)",
            "com.apurebase.kgraphql.schema.model.BaseOperationDef.invoke(BaseOperationDef.kt)",
            "com.apurebase.kgraphql.schema.structure.Field$Function.invoke(Field.kt)",
            "com.apurebase.kgraphql.schema.execution.ParallelRequestExecutor.invoke$kgraphql(ParallelRequestExecutor.kt:342)",
            "com.apurebase.kgraphql.schema.execution.ParallelRequestExecutor.writeOperation(ParallelRequestExecutor.kt:74)",
            "com.apurebase.kgraphql.schema.execution.ParallelRequestExecutor.access$writeOperation(ParallelRequestExecutor.kt:25)",
            "com.apurebase.kgraphql.schema.execution.ParallelRequestExecutor$suspendExecute$2$resultMap$1.invokeSuspend(ParallelRequestExecutor.kt:54)",
            "com.apurebase.kgraphql.schema.execution.ParallelRequestExecutor$suspendExecute$2$resultMap$1.invoke(ParallelRequestExecutor.kt)",
            "com.apurebase.kgraphql.schema.execution.ParallelRequestExecutor$suspendExecute$2$resultMap$1.invoke(ParallelRequestExecutor.kt)",
            "com.apurebase.kgraphql.ExtensionsKt$toMapAsync$2$jobs$1$1.invokeSuspend(Extensions.kt:46)",
            "kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)",
            "kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)",
            "kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)",
            "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)",
            "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)",
            "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)"
          ]
        }
      }
    }
  ]
}

Change 3: Moved serialize (), which was defined as an extension function of GraphQLError, into GraphQLError. option

I want to move serialize () to use GraphQLError outside the KGraphQL plugin. For example, KGraphQL relies on an external plugin for authentication processing. In that case, if an authentication error occurs, 401 will occur and you will not be able to return a response in the general GraphQL error format.

Therefore, I would like to do the following. In the StatusPages plugin, I want to create a GraphQLError and use the serialize () to make it json and respond. For that, serialize () wants to go inside GraphQLError.

install(StatusPages) {
    status(HttpStatusCode.Unauthorized) {
        val ex =  GraphQLError("Unauthorized","AUTHORIZATION_ERROR")
        call.respond(ex.serialize())
    }
}
Nexcius commented 2 years ago

This would be a very welcome change!

MaaxGr commented 2 years ago

Hey @ima9dan. I also implemented extensions in #166, but your solutions seems more advanced, because my solution don't support other types than string for extension values. I will close my merge request and point to that one! :)

@jeggy requested a unit test to prove that the solution works. That probably would also make sense here.