Error Handling in Spring for GraphQL
Let's discuss error handling in Spring for GraphQL. We will also look at the ErrorHandler implementation that's capable of handling the custom and built-in exceptions.
Join the DZone community and get the full member experience.
Join For FreeThe Problem
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 exceptions globally for multiple controllers. Apparently, that is not the case for 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")
And not:
implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:14.0.0")
These are two completely different things, and this should be kept in mind during development and research on different websites.
So what is the problem? Whenever you run a GraphQL query/mutation and your service/facade is throwing an exception — let's say a NotFoundException
— by default, you’re getting this output for the result:
{
"errors": [
{
"message": "INTERNAL_ERROR for 2ce1d7be-86f2-da5d-bdba-aac45f4a534f",
"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 straightaway instead of going through the documentation first. That led me to a journey of trial and errors 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.
Epiphany
After trying everything out, let’s see what the documentation states about exception resolution:
DataFetcherExceptionResolver
is an asynchronous contract. For most implementations, it would be sufficient to extendDataFetcherExceptionResolverAdapter
and override one of itsresolveToSingleError
orresolveToMultipleErrors
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 on GitHub. Here are the DTOs being used:
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
)
data class CourseResponse(
val id: Int?,
val name: String,
val category: String,
val createdAt: String,
val updatedAt: String,
val instructor: InstructorResponse
)
data class InstructorRequest(
@get:NotBlank(message = "must not be blank") val name: String,
)
data class InstructorResponse(
val id: Int?,
val name: String?,
)
And here is their representation in the schema.graphqls
:
type CourseResponse {
id: ID
name: String
category: String
instructor: InstructorResponse
}
input CourseRequest{
name: String
category: String
instructor: InstructorRequest
}
type InstructorResponse {
id: ID
name: String
}
input InstructorRequest {
name: String
}
Now we have our controller:
@Controller
class CourseGraphQLController(val courseFacade: CourseFacade) {
@QueryMapping
fun getCourseById(@Argument id: Int): CourseResponse = courseFacade.findById(id)
@QueryMapping
fun getAllCourses(): List<CourseResponse> = courseFacade.findAll()
@SchemaMapping(typeName = "CourseResponse", field = "instructor")
fun getInstructor(course: CourseResponse): InstructorResponse = course.instructor
@MutationMapping
fun deleteCourseById(@Argument id: Int) = courseFacade.deleteById(id)
@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 — an 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.
Okay, here is the schema mapping for the queries/mutations:
type Query {
getAllCourses: [CourseResponse]!
getCourseById(id: Int): CourseResponse
}
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 following GraphQL query:
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.
Solution
Okay, enough talk; 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.DataFetchingException).build()
}
}
So we extended the DataFetcherExceptionResolverAdapter
and overrode the resolveToSingleError
method to treat our exception the correct way. Basically, it is a translation of the NotFoundException
to GraphQLError
. Now, if we run our query again:
{
"errors": [
{
"message": "Course with id equal to [-999] could not be found!",
"locations": [],
"extensions": {
"classification": "DataFetchingException"
}
}
],
"data": {
"getCourseById": null
}
}
Beautiful, isn’t it?
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)
}
override fun resolveToSingleError(e: Throwable, env: DataFetchingEnvironment): GraphQLError? {
return when (e) {
is NotFoundException -> toGraphQLError(e)
is ConstraintViolationException -> handleConstraintViolationException(e)
is Exception -> 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.DataFetchingException).build()
}
private fun handleConstraintViolationException(e: ConstraintViolationException): GraphQLError? {
val errorMessages = mutableSetOf<String>()
e.constraintViolations.forEach { errorMessages.add("Field '${it.propertyPath}' ${it.message}, but value was [${it.invalidValue}]") }
val message = errorMessages.joinToString("\n")
log.warn("Exception while handling request: $message", e)
return GraphqlErrorBuilder.newError().message(message).errorType(ErrorType.DataFetchingException).build()
}
}
As you can see, the NotFoundException
/Exception
will be simply translated to GraphQLError
(yes, at the moment, the logic’s the same and NotFoundException
may be removed, but I prefer to keep them separated for future possible changes). ConstraintViolationException
is treated separately by constructing a sensible message.
Now, if we run our mutation again, voila!
{
"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 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 you missed it, here is the full project.
P.S. Here is an unrelated tip for the Kotlin users who 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 a 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.
Opinions expressed by DZone contributors are their own.
Comments