GraphQL×Java入門 エラーハンドリング

## はじめに

前回はMutation編ということでMutationの実装例を紹介させてもらいました。

今回は、エラーハンドリングの実装例を紹介します。
ただ、JavaでGraphQLの実装してもらうときのポイントとなるところを紹介するのがメインなので、ソースコードを作り込んでいるわけではないので、実務でそのまま使用するのは避けてくださいmm
(時短で結構適当にロジックや例外処理書いてあります)

## 環境

・SpringBoot:3.3.2
・Java:17
・GraphQL:22.1

## ソースコード

[GraphQL Sample](https://github.com/kehuka11/graphql_sample)

## エラー時のステータスコード

https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#applicationjson

GraphQLはエラー時でも200コード返す仕様みたい。

ここに書いてあるように、ステータスコードは200固定が推奨なので、今回カスタム例外クラスでステータスコード返す必要なし。

## エラーの種類

大きく分けると、以下2つ

・ドメインエラー
 　操作ミスなどで発生するエラー。
 　操作変更によって復帰可能
 
・システムエラー
　システムの異常によるエラー
 　ユーザ操作による復帰が不可能


## ドメインエラーのエラーハンドリング

ドメインエラーは、クライアントからの操作を変更して、エラーを回避できる場合があるので、レスポンスのdata部の中にエラー内容を入れることで、クライアント側で既知のドメインのエラーであることを検知できる。

**スキーマ**

~~~graphql
type Mutation {
    registerBook(title: String!, author: AuthorInput!): Book!
}

input AuthorInput {
    id: ID!
    firstName: String!
    lastName: String!
}

type Book {
    id: ID!
    title: String!
    author: Author
    error: InvalidError
}

interface DomainError {
    message: String
}

type InvalidError implements DomainError{
    message: String
    field: String
}
~~~

**カスタムエクセプションクラス**

~~~java
public class BookRentalException extends RuntimeException {

    public BookRentalException(String message) {
        super(message);
    }
}

public class InvalidBookException extends BookRentalException{

    private final String field;
    public InvalidBookException(String message, String field) {
        super(message);

        this.field = field;
    }
}
~~~

**ドメイン**

~~~java
public record Book(BookId id, String title, Author author) {
    public Book {
        if(title.length() < 1) {
            throw new InvalidBookException("Invalid Book title length", "title");
        }
    }
}
~~~

**ユースケース**

~~~java
public BookResponse registerBook(String title, Author author) {

        Book book = null;
        try {
            UUID uuid = UUID.randomUUID();
            book = new Book(new BookId(uuid.toString()), title, author);
        } catch (InvalidBookException e) {
            return new BookResponse("error-id", title, author, e);
        }

        if (!authorRepository.existById(author.id())) {
            authorRepository.register(author);
        }

        bookRepository.registerBook(book);

        return new BookResponse(book.id().getId().toString(), book.title(), book.author(), null);
    }
~~~

**クエリ**

~~~graphql
mutation {
  registerBook(title: "", 
  author: {
      id: "author-1",
      firstName: "first-test-name"
      lastName: "last-test-name" 
  }) {
    id 
    title
    author {
      id
      firstName
      lastName
    }
    error {
        message
        field
    }
  }
}
~~~

**レスポンス**

~~~json
{
    "data": {
        "registerBook": {
            "id": "error-id",
            "title": "",
            "author": {
                "id": "author-1",
                "firstName": "first-test-name",
                "lastName": "last-test-name"
            },
            "error": {
                "message": "Invalid Book title length",
                "field": "title"
            }
        }
    }
}
~~~

data部にエラー内容を含めることができました！

## システムエラーのエラーハンドリング

**エラー内容**

~~~json
{
    "errors": [
        {
            "message": "INTERNAL_ERROR for c9884c31-4698-8d1e-4c01-80f34f6aa844",
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ],
            "path": [
                "registerBook"
            ],
            "extensions": {
                "classification": "INTERNAL_ERROR"
            }
        }
    ],
    "data": null
}
~~~

システムエラーは`INTERNAL_ERROR`として出力されます。

意図して発生せず、クライアントの操作によって改善する可能性も低いです。
なので、data部に含めて返すのではなく、error部をカスタマイズして、返却する方向で考えます。

 GlobalExceptionHandlerを用意して、リクエストを受け取った以降のクラスで発生したExceptionをキャッチして、GraphQLのerror部の内容をカスタムして返却していきます。

**ExceptionHandler**
 ~~~java
@ControllerAdvice
public class GlobalExceptionHandler {

    @GraphQlExceptionHandler
    public GraphQLError handleCustomException(Exception e) {
        return GraphQLError.newError()
                .errorType(ErrorType.DataFetchingException)
                .message("test error")
                .build();
    }
}
 ~~~

 **エラー結果**

 ~~~java
{
    "errors": [
        {
            "message": "test error",
            "locations": [],
            "extensions": {
                "classification": "DataFetchingException"
            }
        },
        {
            "message": "The field at path '/registerBook' was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value.  The graphql specification requires that the parent field be set to null, or if that is non nullable that it bubble up null to its parent and so on. The non-nullable type is 'Book' within parent type 'Mutation'",
            "path": [
                "registerBook"
            ],
            "extensions": {
                "classification": "NullValueInNonNullableField"
            }
        }
    ],
    "data": null
}
 ~~~

 error部にカスタムエラーを入れることができました。

 ## さいごに

 GraphQLを採用するかにあたって、ざっと必要そうなところをキャッチアップしてみました。
 最近はAWSの環境構築とか構成考えたりばっかりだったので、久しぶりにプログラム書けて楽しかったです笑

 ## 参考文献

 https://spring.pleiades.io/spring-graphql/reference

https://graphql.org/learn/schema/#union-types