简体   繁体   中英

How to handle GraphQL query validation error in Spring Boot

I have a simple Spring Boot project using spring-boot-starter-graphql . This project has one controller that accepts one argument.

@Controller
public class HelloNameController {

    @QueryMapping
    public String hello(@Argument String name) {
        return "Hello " + name;
    }
}

This argument is required.

Graphql schema

 type Query {
     hello (name : String!) : String
  }

When I call this API in the Postman and do not pass this argument the app returns an error. I want to override the message of this error message, but I can't find a way to do it. In the official documentation , it says to implement DataFetcherExceptionResolverAdapter and I've implemented it as a bean

@Configuration
public class GraphQLConfig {

    @Bean
    public DataFetcherExceptionResolver exceptionResolver() {
        return DataFetcherExceptionResolverAdapter.from((ex, env) -> {
            if (ex instanceof CoercingParseValueException) {

                return GraphqlErrorBuilder.newError(env).message("CoercingParseValueException")
                        .errorType(ErrorType.ExecutionAborted).build();
            }
            if (ex instanceof CoercingSerializeException) {
                return GraphqlErrorBuilder.newError(env).message("CoercingSerializeException")
                        .errorType(ErrorType.ExecutionAborted).build();
            } else {
                return null;
            }
        });
    }
}

The problem is that the error never gets to this point. How do I catch this type of error and override the message?

I've asked a similar question on a GitHub. Responses from graphql-java project ( #2866 ) and spring-graphql project ( #415 ) were similar. To summarise at the time of writing it is not possible. Then I've created a "workaround":

First, create a custom exception class that implements GraphQLError.

import graphql.GraphQLError;
import graphql.language.SourceLocation;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.http.HttpStatus;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Getter
@NoArgsConstructor
public class BadRequestException extends RuntimeException implements GraphQLError {

    private HttpStatus status = HttpStatus.BAD_REQUEST;

    private String message = "Resource not found";

    // Below code used for GraphQL only
    private List<SourceLocation> locations;

    public BadRequestException(String message, List<SourceLocation> locations) {
        this.message = message;
        this.locations = locations;
    }

    @Override
    public Map<String, Object> getExtensions() {
        Map<String, Object> customAttributes = new LinkedHashMap<>();
        customAttributes.put("errorCode", this.status.value());
        return customAttributes;
    }

    @Override
    public List<SourceLocation> getLocations() {
        return locations;
    }

    @Override
    public ErrorType getErrorType() {
        return ErrorType.BAD_REQUEST;
    }

    @Override
    public Map<String, Object> toSpecification() {
        return GraphQLError.super.toSpecification();
    }

}

Second, create an interceptor class that implements WebGraphQlInterceptor and annotate it as @Component, so Spring can create it as a bean. Inside this class implement logic to catch the needed error and convert it to the exception class created before

import graphql.ErrorClassification;
import graphql.ErrorType;
import graphql.GraphQLError;
import graphql.validation.ValidationErrorType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.graphql.ResponseError;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Component
public class ErrorInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
        return chain.next(request)
                .map(response -> {
                    log.info("[ErrorInterceptor] Intercepting response... ");

                    List<GraphQLError> graphQLErrors = response.getErrors().stream()
                            .filter(error -> ErrorType.ValidationError.equals(error.getErrorType()))
                            .map(this::resolveException)
                            .collect(Collectors.toList());

                    if (!graphQLErrors.isEmpty()) {
                        log.info("[ErrorInterceptor] Found invalid syntax error! Overriding the message.");
                        return response.transform(builder -> builder.errors(graphQLErrors));
                    }

                    return response;
                });
    }

    private GraphQLError resolveException(ResponseError responseError) {

        ErrorClassification errorType = responseError.getErrorType();

        if (ErrorType.ValidationError.equals(errorType)) {
            String message = responseError.getMessage();
            log.info("[ErrorInterceptor] Returning invalid field error ");

            if (ValidationErrorType.NullValueForNonNullArgument.equals(
                    extractValidationErrorFromErrorMessage(responseError.getMessage()))) {
                String errorMessage =
                        "Field " + StringUtils.substringBetween(message, "argument ", " @") + " cannot be null";
                return new BadRequestException(errorMessage, responseError.getLocations());
            }
        }

        log.info("[ErrorInterceptor] Returning unknown query validation error ");
        return new BadRequestException("Unknown error", responseError.getLocations());
    }

    private ValidationErrorType extractValidationErrorFromErrorMessage(String message) {
        return ValidationErrorType.valueOf(StringUtils.substringBetween(message, "type ", ":"));
    }

}

The only problem with this approach is that all needed information like a type of an error, the field that causes the error, etc. is embedded in the native error message. So to extract the needed parameters we have to parse the string message.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM