Перейти к основному содержимому

Обработка ошибок в GraphQL с Spring Boot

· 10 мин. чтения

1. Обзор

В этом руководстве мы узнаем о параметрах обработки ошибок в GraphQL . Мы посмотрим, что говорит спецификация GraphQL об ответах на ошибки. Следовательно, мы разработаем пример обработки ошибок GraphQL с использованием Spring Boot.

2. Ответ согласно спецификации GraphQL

Согласно спецификации GraphQL, каждый полученный запрос должен возвращать правильно сформированный ответ. Этот правильно сформированный ответ состоит из карты данных или ошибок из соответствующей успешной или неудачной запрошенной операции. Кроме того, ответ может содержать частичные успешные данные результата и ошибки в полях.

Ключевыми компонентами карты ответов являются ошибки , данные и расширения .

Раздел ошибок в ответе описывает любой сбой во время запрошенной операции. Если ошибки не возникает, компонент ошибки не должен присутствовать в ответе. В следующем разделе мы рассмотрим различные типы ошибок, описанные в спецификации.

Раздел данных описывает результат успешного выполнения запрошенной операции. Если операция является запросом, этот компонент является объектом типа корневой операции запроса. С другой стороны, если операция является мутацией, этот компонент является объектом типа операции корня мутации.

Если запрошенная операция завершается с ошибкой еще до ее выполнения из-за отсутствия информации, ошибок проверки или синтаксических ошибок, компонент данных не должен присутствовать в ответе. И если при выполнении операции происходит сбой с неудачным результатом, то компонент данных должен быть нулевым .

Карта ответов может содержать дополнительный компонент, называемый extensions , который является объектом карты. Компонент позволяет разработчикам предоставлять другое пользовательское содержимое в ответе по своему усмотрению. Следовательно, нет никаких дополнительных ограничений на формат его содержания.

Если компонент данных отсутствует в ответе, компонент ошибок должен присутствовать и должен содержать хотя бы одну ошибку. Далее следует указать причины сбоев.

Вот пример ошибки 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. Ошибки запроса

Как следует из названия, ошибки запроса могут возникать до выполнения операции, если есть какие-либо проблемы с самим запросом. Это может быть связано с ошибкой синтаксического анализа данных запроса, проверкой документа запроса, неподдерживаемой операцией или недопустимыми значениями запроса.

Когда возникает ошибка запроса, это указывает на то, что выполнение еще не началось, а это означает, что раздел данных в ответе не должен присутствовать в ответе. Другими словами, ответ содержит только раздел ошибок .

Давайте посмотрим на пример, демонстрирующий случай недопустимого синтаксиса ввода:

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 не должно обнуляться.

Однако информация об одном из транспортных средств имеет нулевое значение отделки , поэтому мы возвращаем только частичные данные — автомобили, значение отделки которых не равно нулю — вместе с ошибкой:

{
"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. Формат ответа об ошибке

Как мы видели ранее, ошибки в ответе представляют собой набор одной или нескольких ошибок. И каждая ошибка должна содержать ключевое сообщение , описывающее причины сбоя, чтобы разработчик клиента мог внести необходимые исправления, чтобы избежать ошибки.

Каждая ошибка также может содержать ключ с именем location , который представляет собой список местоположений, указывающих на строку в запрошенном документе GraphQL, связанную с ошибкой. Каждое местоположение представляет собой карту с ключами: строка и столбец, соответственно, предоставляя номер строки и номер начального столбца связанного элемента.

Другой ключ, который может быть частью ошибки, называется path . Он предоставляет список значений от корневого элемента, отслеживаемого до конкретного элемента ответа, содержащего ошибку. Значением пути может быть строка, представляющая имя поля или индекс элемента ошибки, если значением поля является список. Если ошибка связана с полем с псевдонимом, то значение в пути должно быть псевдонимом.

3.4. Обработка ошибок поля

Независимо от того, возникает ли ошибка поля в поле, допускающем или не допускающем значение NULL, мы должны обрабатывать ее так, как если бы поле вернуло значение null , и ошибка должна быть добавлена в список ошибок .

В случае поля, допускающего значение NULL, значение поля в ответе будет пустым , но ошибки должны содержать эту ошибку поля, описывающую причины сбоя и другую информацию, как показано в предыдущем разделе.

С другой стороны, родительское поле обрабатывает ошибку поля, не допускающую значение NULL. Если родительское поле не может принимать значение NULL, то обработка ошибок распространяется до тех пор, пока мы не достигнем родительского поля, допускающего значение NULL, или корневого элемента.

Точно так же, если поле списка содержит тип, не допускающий значения NULL, и один или несколько элементов списка возвращают null , весь список преобразуется в null . Кроме того, если родительское поле, содержащее поле списка, не может принимать значение NULL, то обработка ошибок распространяется до тех пор, пока мы не достигнем родительского элемента, допускающего значение NULL, или корневого элемента.

По какой-либо причине, если во время разрешения возникает несколько ошибок для одного и того же поля, то для этого поля мы должны добавить только одну ошибку поля в errors .

4. Библиотеки Spring Boot GraphQL

В нашем примере приложения Spring Boot используется модуль graphql-spring-boot-starter , который включает в себя graphql-java-servlet и graphql-java .

Мы также используем модуль graphql-java-tools , который помогает сопоставить схему GraphQL с существующими объектами Java, а для модульных тестов мы используем graphql-spring-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 Boot GraphQL

В этом разделе мы в основном рассмотрим обработку ошибок GraphQL в самом приложении Spring Boot. Мы не будем рассматривать разработку приложений GraphQL Java и GraphQL Spring Boot .

В нашем примере приложения Spring Boot мы будем изменять или запрашивать транспортные средства на основе местоположения или VIN (идентификационного номера транспортного средства). На этом примере мы увидим различные способы реализации обработки ошибок.

Модуль graphql-java-servlet предоставляет интерфейс 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 создает исключение GenericGraphQLError , представляющее InvalidInputException с внутренним сообщением об ошибке сервера.

5.2. Ответ GraphQL с исключением типа GraphQLError

Теперь давайте посмотрим, как будет выглядеть ответ, если мы реализуем наше пользовательское исключение как GraphQLError. 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);
}
}

Как видно из приведенного выше фрагмента кода, мы вернули null для методов getLocations() и getErrorType() , потому что исключение оболочки по умолчанию, 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 в сотрудничестве с командой Java GraphQL разрабатывает новую библиотеку spring-boot-starter-graphql для Spring Boot с GraphQL. Он все еще находится на этапе промежуточного выпуска, а не в общедоступном выпуске (GA).

Как всегда, полный исходный код доступен на GitHub .