0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

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

環境

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

ソースコード

GraphQL Sample

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

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

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

エラーの種類

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

・ドメインエラー
 操作ミスなどで発生するエラー。
 操作変更によって復帰可能

・システムエラー
 システムの異常によるエラー
 ユーザ操作による復帰が不可能

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

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

スキーマ

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
}

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

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;
    }
}

ドメイン

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

ユースケース

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);
    }

クエリ

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

レスポンス

{
    "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部にエラー内容を含めることができました!

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

エラー内容

{
    "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

@ControllerAdvice
public class GlobalExceptionHandler {

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

エラー結果

{
   "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の環境構築とか構成考えたりばっかりだったので、久しぶりにプログラム書けて楽しかったです笑

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?