GraphQL java send custom error in json format
Asked Answered
H

2

9

I am working in an graphql application where I have to send custom error object / message in json irrespective of whether it occurs in servlet or service.

Expected error response

{ errorCode: 400 //error goes here, errorMessage: "my error mesage"}

It will be helpful if someone could guide me to achieve the above requirement.

Hideout answered 29/8, 2018 at 18:24 Comment(0)
M
20

GraphQL specification defines a clear format for the error entry in the response.

According to the spec, it should like this (assuming JSON format is used):

  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [ { "line": 6, "column": 7 } ],
      "path": [ "hero", "heroFriends", 1, "name" ]
      "extensions": {/* You can place data in any format here */}
    }
  ]

So you won't find a GraphQL implementation that allows you to extend it and return some like this in the GraphQL execution result, for example:

  "errors": [
    {
      "errorMessage": "Name for character with ID 1002 could not be fetched.",
      "errorCode": 404
    }
  ]

However, the spec lets you add data in whatever format in the extension entry. So you could create a custom Exception on the server side and end up with a response that looks like this in JSON:

  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [ { "line": 6, "column": 7 } ],
      "path": [ "hero", "heroFriends", 1, "name" ]
      "extensions": {
          "errorMessage": "Name for character with ID 1002 could not be fetched.",
          "errorCode": 404
      }
    }
  ]

It's quite easy to implement this on GraphQL Java, as described in the docs. You can create a custom exception that overrides the getExtensions method and create a map inside the implementation that will then be used to build the content of extensions:

public class CustomException extends RuntimeException implements GraphQLError {
    private final int errorCode;

    public CustomException(int errorCode, String errorMessage) {
        super(errorMessage);

        this.errorCode = errorCode;
    }

    @Override
    public Map<String, Object> getExtensions() {
        Map<String, Object> customAttributes = new LinkedHashMap<>();

        customAttributes.put("errorCode", this.errorCode);
        customAttributes.put("errorMessage", this.getMessage());

        return customAttributes;
    }

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

    @Override
    public ErrorType getErrorType() {
        return null;
    }
}

then you can throw the exception passing in the code and message from inside your data fetchers:

throw new CustomException(400, "A custom error message");

Now, there is another way to tackle this.

Assuming you are working on a Web application, you can return errors (and data, for that matter) in whatever format that you want. Although that is a bit awkward in my opinion. GraphQL clients, like Apollo, adhere to the spec, so why would you want to return a response on any other format? But anyway, there are lots of different requirements out there.

Once you get a hold of an ExecutionResult, you can create a map or object in whatever format you want, serialise that as JSON and return this over HTTP.

Map<String, Object> result = new HashMap<>();

result.put("data", executionResult.getData());

List<Map<String, Object>> errors = executionResult.getErrors()
        .stream()
        .map(error -> {
            Map<String, Object> errorMap = new HashMap<>();

            errorMap.put("errorMessage", error.getMessage());
            errorMap.put("errorCode", 404); // get the code somehow from the error object

            return errorMap;
        })
        .collect(toList());

result.put("errors", errors);

// Serialize "result" and return that.

But again, having a response that doesn't comply with the spec doesn't make sense in most of the cases.

Musicale answered 5/10, 2018 at 9:29 Comment(3)
Thx for your reply. For me it works now for customerrors. But how can i hook up other errors like NonNullableValueCoercedAsNullException ? That exception is throwed by graphql(Apollo).Bonaire
@pipo_dev This isn't working for me. The CustomException is ignored by graphql.servlet.DefaultGraphQLErrorHandler. Do you any other workaround?Bianchi
The field name given in the answer is extension but the spec and the other code in the answer says that it should be extensions. Unfortunately I don't have permission to make one-character edits :)Avocado
B
5

The other posted answer didn't work for me. I found a solution by creating the following classes:

1) A throwable CustomException of GraphQLError type (just like mentioned in another answer).

2) Creating a GraphQLError Adaptor, which is not a Throwable.

3) A custom GraphQLErrorHandler to filter the custom exception.

Step 1:
The below throwable CustomGraphQLException implements GraphQLError because the GraphQLErrorHandler interface accepts errors only of type GraphQLError.

public class CustomGraphQLException extends RuntimeException implements GraphQLError {

    private final int errorCode;
    private final String errorMessage;

    public CustomGraphQLException(int errorCode, String errorMessage) {
        super(errorMessage);
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }

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

    @Override
    public ErrorType getErrorType() {
        return null;
    }

    @Override
    public String getMessage() {
        return this.errorMessage;
    }

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

Step 2:
A non-throwable adaptor of GraphQLError is created to avoid the stack-trace of the above custom exception being passed in the final GraphQL Error Response.

public class GraphQLErrorAdaptor implements GraphQLError {

    private final GraphQLError graphQLError;

    public GraphQLErrorAdaptor(GraphQLError graphQLError) {
        this.graphQLError = graphQLError;
    }

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

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

    @Override
    public String getMessage() {
        return graphQLError.getMessage();
    }

    @Override
    public Map<String, Object> getExtensions() {
        return graphQLError.getExtensions();
    }
}

Step 3:
A custom GraphQLErrorHandler is implemented to filter the custom CustomGraphQLException and avoid its replacement with the default graphQL error response.

public class CustomGraphQLErrorHandler implements GraphQLErrorHandler {

    public CustomGraphQLErrorHandler() { }

    public List<GraphQLError> processErrors(List<GraphQLError> errors) {
        List<GraphQLError> clientErrors = this.filterGraphQLErrors(errors);
        List<GraphQLError> internalErrors = errors.stream()
                .filter(e -> isInternalError(e))
                .map(GraphQLErrorAdaptor::new)
                .collect(Collectors.toList());

        if (clientErrors.size() + internalErrors.size() < errors.size()) {
            clientErrors.add(new GenericGraphQLError("Internal Server Error(s) while executing query"));
            errors.stream().filter((error) -> !this.isClientError(error)
            ).forEach((error) -> {
                if (error instanceof Throwable) {
                    LOG.error("Error executing query!", (Throwable) error);
                } else {
                    LOG.error("Error executing query ({}): {}", error.getClass().getSimpleName(), error.getMessage());
                }

            });
        }
        List<GraphQLError> finalErrors = new ArrayList<>();
        finalErrors.addAll(clientErrors);
        finalErrors.addAll(internalErrors);

        return finalErrors;
    }

    protected List<GraphQLError> filterGraphQLErrors(List<GraphQLError> errors) {
        return errors.stream().filter(this::isClientError).collect(Collectors.toList());
    }

    protected boolean isClientError(GraphQLError error) {
        return !(error instanceof ExceptionWhileDataFetching) && !(error instanceof Throwable);
    }

    protected boolean isInternalError(GraphQLError error) {
        return (error instanceof ExceptionWhileDataFetching) &&
                (((ExceptionWhileDataFetching) error).getException() instanceof CustomGraphQLException);
    }

}

Step 4: Configure the CustomGraphQLErrorHandler in GraphQLServlet. I am assuming you are using spring-boot for this step.

@Configuration
public class GraphQLConfig {

    @Bean
    public ServletRegistrationBean graphQLServletRegistrationBean(
            QueryResolver queryResolver,
            CustomGraphQLErrorHandler customGraphQLErrorHandler) throws Exception {

        GraphQLSchema schema = SchemaParser.newParser()
                .schemaString(IOUtils.resourceToString("/library.graphqls", Charset.forName("UTF-8")))
                .resolvers(queryResolver)
                .build()
                .makeExecutableSchema();

        return new ServletRegistrationBean(new SimpleGraphQLServlet(schema,
                new DefaultExecutionStrategyProvider(), null, null, null,
                customGraphQLErrorHandler, new DefaultGraphQLContextBuilder(), null,
                null), "/graphql");

    }

}

Reference

Bianchi answered 22/3, 2019 at 14:53 Comment(6)
SimpleGraphQLServlet is deprecatedHenryk
I've tried implementing this in Kotlin, but I am getting an error when I implement GraphQLError and inherit from RuntimeException. I'm getting a message saying "Accidental Override: the following declarations have the same JVM signature". This is in regards to overriding getMessage(). Any idea on what I could do to fix it? Thanks.Handknit
@AdlyThebaud I guess its a known issue with Kotlin, when you try to override some class written in java. Refer this - youtrack.jetbrains.com/issue/KT-6653#comment=27-2666539. As a workaround you can write that particular class in java and let all other code be in Kotlin. It will solve your issue.Bianchi
@SahilChhabra thanks. One of my coworkers pointed out that I could annotate the function getMessage() with @Suppress("ACCIDENTAL_OVERRIDE"). That seemed to work, for now.Handknit
Does anyone know how to get the location and path for the query and return that in the error response?Handknit
Does someone know how to do it in Quarkus?Scauper

© 2022 - 2024 — McMap. All rights reserved.