Monday, September 25, 2023

GraphQL Error Handling with Spring Boot 3

 Why HTTP 200 is Different from REST

GraphQL servers typically return an HTTP 200 status code for all responses, regardless of whether the request was successful or encountered an error. This behavior is by design and is different from RESTful APIs, where different HTTP status codes are used to indicate the success or failure of a request.

In GraphQL, errors are typically handled differently than in RESTful APIs. Instead of using HTTP status codes, GraphQL responses include an "errors" field in the JSON response object when there are issues with the request. This "errors" field contains an array of error objects, each of which includes information about the specific error, such as a message and location within the query.



GraphQL response might look like when there are errors:

{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "User not found",
      "locations": [
        {
          "line": 4,
          "column": 3
        }
      ],
      "path": ["user"]
    }
  ]
}

The request to fetch a user encountered an error, and the server responds with an HTTP 200 status code but includes an "errors" field in the response to provide details about the error.

If you're working with a GraphQL client or library, it should be designed to check for errors in the response and handle them accordingly. Typically, successful responses will have a "data" field with the requested data, and errors will be present in the "errors" field.

However we can send customized error messages in the "errors" field in the response and we can send something like BAD_REQUEST or NOT_FOUND etc.

GraphQL Error Handling

DataFetcherExceptionResolverAdapter is a Spring for GraphQL class that allows you to implement custom error handling for your GraphQL API. It is an extension of the DataFetcherExceptionResolver interface, which is used to resolve exceptions that occur during the execution of data fetchers.

pom.xml

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>

Config

public class CustomGraphQLException extends RuntimeException {

    private final int statusCode;
    public CustomGraphQLException(int statusCode, String message) {
        super(message);
        this.statusCode = statusCode;
    }
    public int getStatusCode() {
        return statusCode;
    }

}

DataFetcherExceptionResolverAdapter

Sample example HTTP 404 Not Found and HTTP 400 Bad Request

@Component
public class CustomGraphQLExceptionHandler extends DataFetcherExceptionResolverAdapter {

    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
        if (ex instanceof CustomGraphQLException) {
            ErrorType errorType;
            if(((CustomGraphQLException) ex).getStatusCode()==400){
                errorType = ErrorType.BAD_REQUEST;
                return graphQLError(errorType, (CustomGraphQLException) ex, env);
            }
            if(((CustomGraphQLException) ex).getStatusCode()==404){
                errorType = ErrorType.NOT_FOUND;
                return graphQLError(errorType, (CustomGraphQLException) ex, env);
            }
            else {
                return GraphqlErrorBuilder.newError().build();
            }

        } else {
            return GraphqlErrorBuilder.newError().build();
        }
    }

    private GraphQLError graphQLError(ErrorType errorType, CustomGraphQLException ex,DataFetchingEnvironment env){
        return GraphqlErrorBuilder.newError()
                .errorType(errorType)
                .message(ex.getMessage())
                .path(env.getExecutionStepInfo().getPath())
                .location(env.getField().getSourceLocation())
                .build();
    }
}

Resolver

@QueryMapping
    public Iterable<Author> getAllAuthors(@ContextValue Map<String, List<String>> headers){
        throw new CustomGraphQLException(400, "An error occurred while processing your request.");
    }

GraphQLError with the appropriate classification:

{
    "errors": [
        {
            "message": "An error occurred while processing your request.",
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ],
            "path": [
                "getAllAuthors"
            ],
            "extensions": {
                "classification": "BAD_REQUEST"
            }
        }
    ],
    "data": {
        "getAllAuthors": null
    }
}

GraphQLError without classification:

@QueryMapping
    public Iterable<Author> getAllAuthors(@ContextValue Map<String, List<String>> headers){
        throw new CustomGraphQLException(401, "Unauthorized");
    }
{
    "errors": [
        {
            "message": "INTERNAL_ERROR for 54ab7ea3-3bc1-a5b1-0c1c-e45fbda75659",
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ],
            "path": [
                "getAllAuthors"
            ],
            "extensions": {
                "classification": "INTERNAL_ERROR"
            }
        }
    ],
    "data": {
        "getAllAuthors": null
    }
}

Complete exist CRUD example:

Here on GitHub.















Creating REST APIs with OpenAPI, Spring Boot 3.3.3, Java 21, and Jakarta

 Introduction In today's software landscape, designing robust and scalable REST APIs is a crucial aspect of application development. Wit...