Skip to content

Commit

Permalink
Merge pull request #1905 from Netflix/fix-validation-error
Browse files Browse the repository at this point in the history
Convert graphql-java validation errors to TypedGraphQL error
  • Loading branch information
srinivasankavitha committed May 9, 2024
2 parents fe2d40c + 58eca50 commit c3e01f7
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ class DgsGraphQLMetricsInstrumentation(
val errorDetail = errorDetailExtension(error)
when (error) {
is ValidationError -> {
errorPath = error.queryPath.orEmpty()
errorPath = error.path.orEmpty()
errorType = ErrorType.BAD_REQUEST.name
}
is InvalidSyntaxError -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ class MicrometerServletSmokeTest {
.and("gql.operation.name", "anonymous")
.and("gql.query.complexity", "none")
.and("gql.query.sig.hash", "none")
.and("gql.errorDetail", "none")
.and("gql.errorDetail", "INVALID_ARGUMENT")
.and("gql.errorCode", "BAD_REQUEST")
.and("gql.path", "[hello]")
.and("outcome", "failure")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ open class DgsAutoConfiguration(
return GraphQLContextContributorInstrumentation(graphQLContextContributors.orderedStream().toList())
}

@Bean
open fun graphqlJavaErrorInstrumentation(): Instrumentation {
return GraphQLJavaErrorInstrumentation()
}

@Bean
@ConditionalOnMissingBean
open fun dgsQueryExecutor(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.netflix.graphql.dgs.internal

import com.netflix.graphql.types.errors.ErrorDetail
import com.netflix.graphql.types.errors.ErrorType
import com.netflix.graphql.types.errors.TypedGraphQLError
import graphql.ExecutionResult
import graphql.GraphQLError
import graphql.execution.instrumentation.InstrumentationState
import graphql.execution.instrumentation.SimplePerformantInstrumentation
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
import graphql.validation.ValidationError
import graphql.validation.ValidationErrorType
import java.util.concurrent.CompletableFuture

class GraphQLJavaErrorInstrumentation : SimplePerformantInstrumentation() {
override fun instrumentExecutionResult(executionResult: ExecutionResult, parameters: InstrumentationExecutionParameters?, state: InstrumentationState?): CompletableFuture<ExecutionResult> {
if (executionResult.errors.isNotEmpty()) {
val newExecutionResult = ExecutionResult.newExecutionResult().from(executionResult)
val graphqlErrors: MutableList<GraphQLError> = mutableListOf()

executionResult.errors.forEach { error ->
// put in the classification unless it's already there since graphql-java errors contain this field
val extensions = (if (error.extensions != null) error.extensions else emptyMap<String, Any>()).toMutableMap()
if (!extensions.containsKey("classification")) {
val errorClassification = error.errorType
extensions["classification"] = errorClassification.toSpecification(error)
}

if (error.errorType == graphql.ErrorType.ValidationError || error.errorType == graphql.ErrorType.InvalidSyntax ||
error.errorType == graphql.ErrorType.NullValueInNonNullableField || error.errorType == graphql.ErrorType.OperationNotSupported
) {
val path = if (error is ValidationError) error.queryPath else error.path
val graphqlErrorBuilder = TypedGraphQLError
.newBadRequestBuilder()
.locations(error.locations)
.path(path)
.message(error.message)
.extensions(extensions)

if (error is ValidationError) {
if (error.validationErrorType == ValidationErrorType.FieldUndefined) {
graphqlErrorBuilder.errorDetail(ErrorDetail.Common.FIELD_NOT_FOUND)
} else {
graphqlErrorBuilder.errorDetail(ErrorDetail.Common.INVALID_ARGUMENT)
}
}

if (error.errorType == graphql.ErrorType.OperationNotSupported) {
graphqlErrorBuilder.errorDetail(ErrorDetail.Common.INVALID_ARGUMENT)
}
graphqlErrors.add(graphqlErrorBuilder.build())
} else if (error.errorType == graphql.ErrorType.DataFetchingException || error.errorType == graphql.ErrorType.ExecutionAborted) {
val graphqlErrorBuilder = TypedGraphQLError
.newBuilder()
.errorType(ErrorType.INTERNAL)
.errorDetail(ErrorDetail.Common.SERVICE_ERROR)
.locations(error.locations)
.message(error.message)
.extensions(error.extensions)
graphqlErrors.add(graphqlErrorBuilder.build())
} else {
graphqlErrors.add(error)
}
}
return CompletableFuture.completedFuture(newExecutionResult.errors(graphqlErrors).build())
}
return super.instrumentExecutionResult(executionResult, parameters, state)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright 2024 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.graphql.dgs

import com.netflix.graphql.dgs.internal.GraphQLJavaErrorInstrumentation
import graphql.GraphQL
import graphql.execution.instrumentation.Instrumentation
import graphql.schema.StaticDataFetcher
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.SchemaGenerator
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeRuntimeWiring
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class GraphQLJavaErrorInstrumentationTest {

private lateinit var graphqlJavaErrorInstrumentation: Instrumentation

private val schema = """
type Query{
hello(name: String!): String
}
""".trimMargin()

@BeforeEach
fun setup() {
graphqlJavaErrorInstrumentation = GraphQLJavaErrorInstrumentation()
}

@Test
fun `Validation errors are not added for valid queries`() {
val graphQL: GraphQL = buildGraphQL(schema)
val result = graphQL.execute(
"""
{
hello(name: "xyz")
}
""".trimIndent()
)

Assertions.assertThat(result.isDataPresent).isTrue
val data = result.getData<Map<String, String>>()
Assertions.assertThat(data["hello"]).isEqualTo("hello there!")
}

@Test
fun `Validation errors contain errorDetail and errorType in the extensions for invalid fields`() {
val graphQL: GraphQL = buildGraphQL(schema)
val result = graphQL.execute(
"""
{
InvalidField
}
""".trimIndent()
)

Assertions.assertThat(result.isDataPresent).isFalse
Assertions.assertThat(result.errors.size).isEqualTo(1)
Assertions.assertThat(result.errors[0].extensions.keys.containsAll(listOf("classification", "errorDetail", "errorType")))
Assertions.assertThat(result.errors[0].extensions["classification"]).isEqualTo("ValidationError")
Assertions.assertThat(result.errors[0].extensions["errorType"]).isEqualTo("BAD_REQUEST")
Assertions.assertThat(result.errors[0].extensions["errorDetail"]).isEqualTo("FIELD_NOT_FOUND")
}

@Test
fun `Validation errors contain errorDetail and errorType in the extensions for invalid input`() {
val graphQL: GraphQL = buildGraphQL(schema)
val result = graphQL.execute(
"""
{
hello
}
""".trimIndent()
)

Assertions.assertThat(result.isDataPresent).isFalse
Assertions.assertThat(result.errors.size).isEqualTo(1)
Assertions.assertThat(result.errors[0].extensions.keys.containsAll(listOf("classification", "errorType")))
Assertions.assertThat(result.errors[0].extensions["classification"]).isEqualTo("ValidationError")
Assertions.assertThat(result.errors[0].extensions["errorType"]).isEqualTo("BAD_REQUEST")
}

@Test
fun `Validation errors contain errorDetail and errorType in the extensions for invalid syntax`() {
val graphQL: GraphQL = buildGraphQL(schema)
val result = graphQL.execute(
"""
{
hello(
}
""".trimIndent()
)

Assertions.assertThat(result.isDataPresent).isFalse
Assertions.assertThat(result.errors.size).isEqualTo(1)
Assertions.assertThat(result.errors[0].extensions.keys.containsAll(listOf("classification", "errorDetail", "errorType")))
Assertions.assertThat(result.errors[0].extensions["classification"]).isEqualTo("InvalidSyntax")
Assertions.assertThat(result.errors[0].extensions["errorType"]).isEqualTo("BAD_REQUEST")
}

@Test
fun `Error contains errorDetail and errorType in the extensions for invalid operation`() {
val graphQL: GraphQL = buildGraphQL(schema)
val result = graphQL.execute(
"""
mutation {
hell
}
""".trimIndent()
)

Assertions.assertThat(result.isDataPresent).isFalse
Assertions.assertThat(result.errors.size).isEqualTo(1)
Assertions.assertThat(result.errors[0].extensions.keys.containsAll(listOf("classification", "errorDetail", "errorType")))
Assertions.assertThat(result.errors[0].extensions["classification"]).isEqualTo("OperationNotSupported")
Assertions.assertThat(result.errors[0].extensions["errorType"]).isEqualTo("BAD_REQUEST")
Assertions.assertThat(result.errors[0].extensions["errorDetail"]).isEqualTo("INVALID_ARGUMENT")
}

@Test
fun `Multiple validation errors contain errorDetail and errorType in the extensions for multiple invalid fields`() {
val graphQL: GraphQL = buildGraphQL(schema)
val result = graphQL.execute(
"""
{
InvalidField
helloInvalid
}
""".trimIndent()
)

Assertions.assertThat(result.isDataPresent).isFalse
Assertions.assertThat(result.errors.size).isEqualTo(2)
Assertions.assertThat(result.errors[0].extensions.keys.containsAll(listOf("classification", "errorDetail", "errorType")))
Assertions.assertThat(result.errors[0].extensions["classification"]).isEqualTo("ValidationError")
Assertions.assertThat(result.errors[0].extensions["errorType"]).isEqualTo("BAD_REQUEST")
Assertions.assertThat(result.errors[0].extensions["errorDetail"]).isEqualTo("FIELD_NOT_FOUND")
Assertions.assertThat(result.errors[1].extensions.keys.containsAll(listOf("class", "errorDetail", "errorType")))
Assertions.assertThat(result.errors[1].extensions["classification"]).isEqualTo("ValidationError")
Assertions.assertThat(result.errors[1].extensions["errorType"]).isEqualTo("BAD_REQUEST")
Assertions.assertThat(result.errors[1].extensions["errorDetail"]).isEqualTo("FIELD_NOT_FOUND")
}

private fun buildGraphQL(schema: String): GraphQL {
val schemaParser = SchemaParser()
val typeDefinitionRegistry = schemaParser.parse(schema)
val runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query") { builder: TypeRuntimeWiring.Builder ->
builder.dataFetcher("hello", StaticDataFetcher("hello there!"))
}
.build()
val schemaGenerator = SchemaGenerator()
val graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring)

return GraphQL.newGraphQL(graphQLSchema).instrumentation(graphqlJavaErrorInstrumentation).build()
}
}

0 comments on commit c3e01f7

Please sign in to comment.