Error Handling in Spring For GraphQL
Error Handling in Spring For GraphQL
GraphQL
Recently I wrote some GraphQL endpoints and got a bit blocked when I came to the error handling
mechanism. Usually when writing REST endpoints you either go for a particular @ExceptionHandler
for your controller or you go for the @ControllerAdvice to handle exception handling globally for
multiple controllers. Apparently that is not the case for the GraphQL, there is a completely different
approach for handling errors.
First, the most important thing that I should mention is that I am using
implementation("org.springframework.boot:spring-boot-starter-graphql")
implementation(“com.graphql-java-kickstart:graphql-spring-boot-starter:14.0.0”)
these are 2 completely different things and this should be kept in mind, during development and
research on different websites.
So what it the problem? Whenever you run a GraphQL query/mutation and your service/facade is
throwing an exception - by default you’re getting this output for the result.
"errors": [
"locations": [
"line": 1,
"column": 13
],
"path": [
"deleteCourseById"
],
"extensions": {
"classification": "INTERNAL_ERROR"
}
],
"data": {
"deleteCourseById": null
Meh, that is not intuitive at all! We miss the exception message, right? This needs to be fixed, I want
to be able to provide the exception message and in certain scenarios be able to override the
exception message for some exceptions and display it.
My biggest mistake was to google it straight-way instead of going through the documentation first
https://docs.spring.io/spring-graphql/docs/current/reference/html/ . That led me to a journey of try
and fails as I’ve never seen before, and all of that is because most of the research ecosystem is filled
with QA and tutorials for the com.graphql-java-kickstart:graphql-spring-boot-
starter library or io.leangen.graphql library, and very little is to be found about Spring for
GraphQL. There are lots of valid answers about the error handling either by implementing the
GraphQLError or by implementing a custom GraphQLErrorHandler or by enabling some kind of
property and so on, but none of them work in Spring for GraphQL as it is a completely different
library.
After trying everything out, let’s see what the documentation states https://docs.spring.io/spring-
graphql/docs/current/reference/html/#execution-exceptions :
DataFetcherExceptionResolver is an asynchronous contract. For most
implementations, it would be sufficient to
extend DataFetcherExceptionResolverAdapter and override one of
its resolveToSingleError or resolveToMultipleErrors methods that resolve
exceptions synchronously.
Wow, how simple is that? Lesson learned. Always check documentation first.
In order to demonstrate the error handling in Spring for GraphQL, let’s configure a mini project
about courses and instructors. For this purpose I used Kotlin, but the solution would work in Java as
well. For the sake of conciseness lots of classes won’t be shown here, but you can go ahead and take
a look at the full source code here https://github.com/theFaustus/course-catalog. Here are the DTOs
being used
input CourseRequest{
name: String
category: String
instructor: InstructorRequest
}
type InstructorResponse {
id: ID
name: String
}
input InstructorRequest {
name: String
}
@Controller
class CourseGraphQLController(val courseFacade: CourseFacade) {
@QueryMapping
fun getCourseById(@Argument id: Int): CourseResponse =
courseFacade.findById(id)
@QueryMapping
fun getAllCourses(): List<CourseResponse> = courseFacade.findAll()
@MutationMapping
fun createCourse(@Valid @Argument request: CourseRequest):
CourseResponse = courseFacade.save(request)
}
Just for the sake of mentioning, Spring for GraphQL is merely providing support for GraphQL Java in
more opinionated way – annotation based approach. So instead of implementing
GraphQLQueryResolver/ GraphQLMutationResolver we use @QueryMapping and
@MutationMapping alongside with @Argument to resolve the method arguments. Also there is
@SchemaMapping (@QueryMapping/@MutationMapping’s parent) which allows a method to act as
the DataFetcher for a field from the schema mapping.
type Mutation {
deleteCourseById(id: Int): Boolean
createCourse(request: CourseRequest): CourseResponse
}
In order to get a little context about the errors here is my generic NotFoundException thrown from
the service.
class NotFoundException(clazz: KClass<*>, property: String, propertyValue:
String) :
RuntimeException("${clazz.java.simpleName} with $property equal to
[$propertyValue] could not be found!")
So by running the
query { getCourseById(id: -999) {
id
name
instructor {
id
}
}}
I was expecting to get something like Course with id equal to [-999] could not be found! But that was
not the case as we’ve seen at the beginning.
Okay time to fix this. Here is the required subclass according to the documentation.
@Component
class GraphQLExceptionHandler : DataFetcherExceptionResolverAdapter() {
companion object {
private val log: Logger = LoggerFactory.getLogger(this::class.java)
}
override fun resolveToSingleError(e: Throwable, env:
DataFetchingEnvironment): GraphQLError? {
return when (e) {
is NotFoundException -> toGraphQLError(e)
else -> super.resolveToSingleError(e, env)
}
}
private fun toGraphQLError(e: Throwable): GraphQLError? {
log.warn("Exception while handling request: ${e.message}", e)
return
GraphqlErrorBuilder.newError().message(e.message).errorType(ErrorType.DataF
etchingException).build()
}
}
"errors": [
"locations": [],
"extensions": {
"classification": "DataFetchingException"
],
"data": {
"getCourseById": null
But wait there is more, this here is a custom exception, what about some built-in exceptions like the
ConstraintViolationException which is thrown when the @Valid is invalidated. As you’ve seen my
CourseRequest’s name is annotated with @NotBlank
data class CourseRequest(
@get:NotBlank(message = "must not be blank") val name: String,
@get:NotBlank(message = "must not be blank") val category: String,
val instructor: InstructorRequest
)
What happens when I try to create a Course with an empty name? Like this?
mutation { createCourse(
request: {
name: "",
category: "DEVELOPMENT",
instructor: {
name: "Thomas William"
}
}) {
id
name
}}
Oh God, no… Again that INTERNAL_ERROR message, but no worries, with our
GraphQLExceptionHandler in place it is a matter of adding a new exception to be handled. Also just
for safety, I’ll add the Exception there too, as the times comes new specializations can be added, but
by default for untreated exception the exception message always will be shown. So here is our new
implementation
@Component
class GraphQLExceptionHandler : DataFetcherExceptionResolverAdapter() {
companion object {
private val log: Logger = LoggerFactory.getLogger(this::class.java)
}
"errors": [
"message": "Field 'createCourse.request.name' must not be blank, but value was []",
"locations": [],
"extensions": {
"classification": "DataFetchingException"
],
"data": {
"createCourse": null
Conclusion
In this article we discussed about error handling in Spring for GraphQL and we looked at the
implementation of ErrorHandler that is capable of handling both the custom exception and the built-
in exceptions. And we learned an important lesson: Always check the documentation first!.
That’s all folks, hope that you liked it. In case that you missed it, here is the full project
https://github.com/theFaustus/course-catalog
P.S. here is an unrelated tip for the Kotlin users that are still trying to implement the GraphQLError
and extend the RuntimeException and getting the “Accidental override: The following declarations
have the same JVM signature (getMessage()Ljava/lang/String;):”. The dirty workaround is to have it
implemented in Java and have single java class in a 100% Kotlin project. The elegant workaround is
to extend the newly created GraphqlErrorException specifically created for Kotlin users as per the
opened github issue https://github.com/graphql-java/graphql-java/issues/1690. Lovely, isn’t it?