1. 概要

このチュートリアルでは、GraphQLのエラー処理オプションについて学習します。 GraphQL仕様がエラー応答について何を言っているかを見ていきます。 したがって、Spring Bootを使用したGraphQLエラー処理の例を開発します。

2. GraphQL仕様ごとの応答

GraphQL仕様に従って、受信したすべてのリクエストは整形式の応答を返す必要があります。 この整形式の応答は、それぞれの成功または失敗した要求された操作からのデータまたはエラーのマップで構成されます。 さらに、応答には、部分的に成功した結果データとフィールドエラーが含まれる場合があります。

応答マップの主要なコンポーネントは、エラーデータ、および拡張機能です。

応答のエラーセクションは、要求された操作中の障害について説明しています。 エラーが発生しない場合は、エラーコンポーネントが応答に含まれていてはなりません。 次のセクションでは、仕様に記載されているさまざまな種類のエラーについて説明します。

データセクションは、要求された操作が正常に実行された結果を示します。 操作がクエリの場合、このコンポーネントはクエリルート操作タイプのオブジェクトです。 一方、操作がミューテーションの場合、このコンポーネントはミューテーションルート操作タイプのオブジェクトです。

情報の欠落、検証エラー、または構文エラーが原因で、実行前でも要求された操作が失敗した場合は、データコンポーネントが応答に存在していてはなりません。 また、操作の実行中に操作が失敗して結果が失敗した場合は、dataコンポーネントはnullである必要があります。

応答マップには、マップオブジェクトであるextendsと呼ばれる追加コンポーネントが含まれる場合があります。 このコンポーネントにより、実装者は、適切と思われる応答で他のカスタムコンテンツを提供できます。 したがって、コンテンツ形式に追加の制限はありません。

データコンポーネントが応答に存在しない場合は、エラーコンポーネントが存在し、少なくとも1つのエラーが含まれている必要があります。さらに、失敗の理由を示す必要があります。

GraphQLエラーの例を次に示します。

mutation {
  addVehicle(vin: "NDXT155NDFTV59834", year: 2021, make: "Toyota", model: "Camry", trim: "XLE",
             location: {zipcode: "75024", city: "Dallas", state: "TX"}) {
    vin
    year
    make
    model
    trim
  }
}

一意性制約に違反した場合のエラー応答は次のようになります。

{
  "data": null,
  "errors": [
    {
      "errorType": "DataFetchingException",
      "locations": [
        {
          "line": 2,
          "column": 5,
          "sourceName": null
        }
      ],
      "message": "Failed to add vehicle. Vehicle with vin NDXT155NDFTV59834 already present.",
      "path": [
        "addVehicle"
      ],
      "extensions": {
        "vin": "NDXT155NDFTV59834"
      }
    }
  ]
}

3. GraphQL仕様ごとのエラー応答コンポーネント

応答のエラーセクションは、空でないエラーのリストであり、それぞれがマップです。

3.1. リクエストエラー

名前が示すように、リクエスト自体に問題がある場合、オペレーションの実行前にリクエストエラーが発生する可能性があります。リクエストデータの解析の失敗、リクエストドキュメントの検証、サポートされていない操作、または無効なリクエストが原因である可能性があります値。

要求エラーが発生した場合、これは実行が開始されていないことを示します。つまり、応答のdataセクションが応答に存在してはなりません。 つまり、応答にはエラーセクションのみが含まれます。

無効な入力構文の場合を示す例を見てみましょう。

query {
  searchByVin(vin: "error) {
    vin
    year
    make
    model
    trim
  }
}

構文エラーに対するリクエストエラーの応答は次のとおりです。この場合は引用符がありません。

{
  "data": null,
  "errors": [
    {
      "message": "Invalid Syntax",
      "locations": [
        {
          "line": 5,
          "column": 8,
          "sourceName": null
        }
      ],
      "errorType": "InvalidSyntax",
      "path": null,
      "extensions": null
    }
  ]
}

3.2. フィールドエラー

フィールドエラーは、その名前が示すように、値を期待されるタイプに強制変換できないか、特定のフィールドの値解決中に内部エラーが発生したために発生する可能性があります。実行中にフィールドエラーが発生することを意味します要求された操作の。

フィールドエラーの場合、要求された操作の実行は続行され、部分的な結果が返されます。つまり、応答のデータセクションが、エラーセクション。

別の例を見てみましょう:

query {
  searchAll {
    vin
    year
    make
    model
    trim
  }
}

今回は、GraphQLスキーマによるとnull許容ではないと思われるVehicle Trimフィールドを含めました。

ただし、車両の情報の1つにnullの Trim 値があるため、 Trim値がnullではない車両の部分的なデータとエラーのみが返されます。

{
  "data": {
    "searchAll": [
      null,
      {
        "vin": "JTKKU4B41C1023346",
        "year": 2012,
        "make": "Toyota",
        "model": "Scion",
        "trim": "Xd"
      },
      {
        "vin": "1G1JC1444PZ215071",
        "year": 2000,
        "make": "Chevrolet",
        "model": "CAVALIER VL",
        "trim": "RS"
      }
    ]
  },
  "errors": [
    {
      "message": "Cannot return null for non-nullable type: 'String' within parent 'Vehicle' (/searchAll[0]/trim)",
      "path": [
        "searchAll",
        0,
        "trim"
      ],
      "errorType": "DataFetchingException",
      "locations": null,
      "extensions": null
    }
  ]
}

3.3. エラー応答形式

前に見たように、応答の errors は、1つ以上のエラーのコレクションです。 また、すべてのエラーには、クライアント開発者がエラーを回避するために必要な修正を行えるように、失敗の理由を説明する重要なメッセージが含まれている必要があります。

各エラーには、locations というキーも含まれる場合があります。これは、エラーに関連付けられた、要求されたGraphQLドキュメント内の行を指す場所のリストです。 各場所は、関連する要素の行番号と開始列番号を提供する、それぞれ行と列のキーを持つマップです。

エラーの一部である可能性のある他のキーはパスと呼ばれます。エラーのある応答の特定の要素までトレースされたルート要素からの値のリストを提供します。 path 値は、フィールド値がリストの場合、フィールド名またはエラー要素のインデックスを表す文字列にすることができます。 エラーがエイリアス名のフィールドに関連している場合、pathの値はエイリアス名である必要があります。

3.4. フィールドエラーの処理

null許容フィールドまたは非null許容フィールドでフィールドエラーが発生したかどうかにかかわらず、フィールドが null を返したかのように処理する必要があり、エラーをerrorsリストに追加する必要があります。

null許容フィールドの場合、応答のフィールドの値は null になりますが、前のセクションで見たように、 errors には、障害の理由やその他の情報を説明するこのフィールドエラーが含まれている必要があります。

一方、親フィールドはnull許容でないフィールドエラーを処理します。 親フィールドがnull許容でない場合、エラー処理は、null許容の親フィールドまたはルート要素に到達するまで伝播されます。

同様に、リストフィールドにnull許容型が含まれていて、1つ以上のリスト要素が null を返す場合、リスト全体がnullに解決されます。 さらに、リストフィールドを含む親フィールドがnull許容でない場合、エラー処理はnull許容の親またはルート要素に到達するまで伝播されます。

何らかの理由で、解決中に同じフィールドで複数のエラーが発生した場合、そのフィールドでは、エラーに1つのフィールドエラーのみを追加する必要があります

4. Spring BootGraphQLライブラリ

Spring Bootアプリケーションの例では、graphql-spring-boot-starterモジュールを使用しています。これによりgraphql-java-servletgraphql-java

また、graphql-java-tools モジュールを使用しています。これは、GraphQLスキーマを既存のJavaオブジェクトにマップするのに役立ちます。ユニットテストには、graphql-[を使用します。 X164X] -boot-starter-test

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>

また、テストにはgraphql-spring-boot-starter-testを使用します。

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-spring-boot-starter-test</artifactId>
    <version>5.0.2</version>
    <scope>test</scope>
</dependency>

5. Spring BootGraphQLエラー処理

このセクションでは、主にSpring Bootアプリケーション自体でのGraphQLエラー処理について説明します。 GraphQLJavaおよびGraphQLSpring Bootアプリケーションの開発については説明しません。

Spring Bootアプリケーションの例では、場所またはVIN(車両識別番号)のいずれかに基づいて車両を変更または照会します。 この例を使用して、エラー処理を実装するさまざまな方法を見ていきます。

The graphql-java-サーブレットモジュールはと呼ばれるインターフェースを提供します GraphQLErrorHandler。 私たちはそれの実装を提供することができます。

次のサブセクションでは、graphql-java-servletモジュールがgraphql- java モジュールのコンポーネントを使用して、例外またはエラーを処理する方法を説明します。

5.1. 標準例外を伴うGraphQL応答

通常、RESTアプリケーションでは、RuntimeExceptionまたはThrowableを拡張して、カスタムランタイム例外クラスを作成します。

public class InvalidInputException extends RuntimeException {
    public InvalidInputException(String message) {
        super(message);
    }
}

このアプローチでは、GraphQLエンジンが次の応答を返すことがわかります。

{
  "data": null,
  "errors": [
    {
      "message": "Internal Server Error(s) while executing query",
      "path": null,
      "extensions": null
    }
  ]
}

上記のエラー応答では、エラーの詳細が含まれていないことがわかります。

デフォルトでは、カスタム例外はSimpleDataFetcherExceptionHandlerクラスによって処理されます。 元の例外を、ソースの場所と実行パス(存在する場合)とともに、次のような別の例外にラップします。 ExceptionWhileDataFetching。 次に、エラーをに追加しますエラーコレクション。 次に、 ExceptionWhileDataFetching は、GraphQLErrorインターフェースを実装します。

SimpleDataFetcherExceptionHandler ハンドラーの後、DefaultGraphQLErrorHandlerと呼ばれる別のハンドラーがエラーコレクションを処理します。 タイプGraphQLErrorのすべての例外をクライアントエラーとして分離します。 ただし、それに加えて、他のすべての非クライアントエラー(存在する場合)に対してGenericGraphQLError例外も作成します。

上記の例では、 InvalidInputException は、 RuntimeException のみを拡張し、 GraphQLError を実装していないため、クライアントエラーではありません。 その結果、 DefaultGraphQLErrorHandler handlerは、内部サーバーエラーメッセージを含むInvalidInputExceptionを表すGenericGraphQLError例外を作成します。

5.2. タイプGraphQLErrorを除くGraphQL応答

次に、カスタム例外を次のように実装した場合の応答がどのようになるかを見てみましょう。 GraphQLError。  The GraphQLError を実装することにより、エラーに関する詳細情報を提供できるようにするインターフェイスです。 getExtensions() 方法。

カスタム例外を実装しましょう:

public class AbstractGraphQLException extends RuntimeException implements GraphQLError {
    private Map<String, Object> parameters = new HashMap();

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

    public AbstractGraphQLException(String message, Map<String, Object> additionParams) {
        this(message);
        if (additionParams != null) {
            parameters = additionParams;
        }
    }

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

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

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

    @Override
    public Map<String, Object> getExtensions() {
        return this.parameters;
    }
}
public class VehicleAlreadyPresentException extends AbstractGraphQLException {

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

    public VehicleAlreadyPresentException(String message, Map<String, Object> additionParams) {
        super(message, additionParams);
    }
}

上記のコードスニペットでわかるように、デフォルトのラッパー例外のため、 getLocations()および getErrorType()メソッドに対してnullが返されました。 ExceptionWhileDataFetching は、カスタムラップされた例外の getMesssage()および getExtensions()メソッドのみを呼び出します。

前のセクションで見たように、SimpleDataFetcherExceptionHandlerクラスはデータフェッチエラーを処理します。 graphql- javaライブラリがパス、場所、エラータイプの設定にどのように役立つかを見てみましょう。

以下のコードスニペットは、GraphQLエンジンの実行がDataFetcherExceptionHandlerParametersクラスを使用してエラーフィールドの場所とパスを設定することを示しています。 そして、これらの値はコンストラクター引数としてExceptionWhileDataFetchingに渡されます。

...
public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {
        Throwable exception = handlerParameters.getException();
        SourceLocation sourceLocation = handlerParameters.getField().getSourceLocation();
        ExecutionPath path = handlerParameters.getPath();

        ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(path, exception, sourceLocation);
        handlerParameters.getExecutionContext().addError(error);
        log.warn(error.getMessage(), exception);
}
...

ExceptionWhileDataFetching クラスのスニペットを見てみましょう。 ここで、エラータイプがDataFetchingExceptionであることがわかります。

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

@Override
public List<Object> getPath() {
    return path;
}

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

6. 結論

このチュートリアルでは、さまざまなタイプのGraphQLエラーについて学習しました。 また、仕様に従ってGraphQLエラーをフォーマットする方法も確認しました。 その後、Spring Bootアプリケーションにエラー処理を実装しました。

Spring チームは、 GraphQL Java チームと共同で、新しいライブラリ spring-boot-starter-graphql[を開発していることに注意してください。 X157X]、GraphQLを使用したSpring Bootの場合。 まだマイルストーンリリースフェーズにあり、一般提供(GA)リリースではありません。

いつものように、完全なソースコードはGitHubから入手できます。