JavaRush /Курсы /Модуль 5. Spring /Лекция 289: Валидация и обработка ошибок в GraphQL

Лекция 289: Валидация и обработка ошибок в GraphQL

Модуль 5. Spring
29 уровень , 8 лекция
Открыта

В реальном мире, как и в программировании, никто не застрахован от ошибок. Клиенты могут отправить неверные данные, база данных может вести себя странно, а разработчик — ну, человек же, может забыть что-то важное. Именно поэтому валидация и обработка ошибок являются критическими аспектами любого API.

GraphQL разработан таким образом, чтобы минимизировать количество ошибок за счёт строгой типизации и валидации схемы. Однако этого недостаточно, особенно при работе с пользовательскими запросами. Нам нужны инструменты, чтобы:

  1. Проверять входные данные на соответствие бизнес-логике.
  2. Формировать информативные сообщения об ошибках.
  3. Локализовать ошибки.
  4. Логировать проблемы для дальнейшего анализа.

Давайте разбираться, как это делается в контексте GraphQL на Spring.


Встроенная валидация в GraphQL

GraphQL на самом базовом уровне использует схему для автоматической валидации запросов. Например, если в схеме указан тип Int, GraphQL автоматически отклонит строку, отправленную в качестве этого поля.

Пример схемы с валидацией типов:


type Query {
    getUserById(id: Int!): User
}

type User {
    id: Int!
    name: String!
    email: String
}

В этом примере поле id должно быть целым числом, и оно обязательно для передачи. GraphQL сам проверяет это до выполнения запроса. Однако, при более сложных сценариях нам нужно задействовать дополнительные инструменты.


Кастомная валидация входных данных

Иногда базовой проверки типа недостаточно. Например, вы хотите убедиться, что ID положительный, имя не слишком длинное, а email введён в правильном формате. Для этих случаев Spring предоставляет богатый инструментарий валидации.

Использование аннотации @Valid

Мы можем использовать javax.validation для проверки входных данных. Рассмотрим пример:

1. Изменим схему:


type Mutation {
    createUser(input: CreateUserInput!): User
}

input CreateUserInput {
    name: String!
    email: String!
    age: Int
}

type User {
    id: Int!
    name: String!
    email: String
    age: Int
}

2. Создадим DTO для обработки входных данных:


import jakarta.validation.constraints.*;

public class CreateUserInput {
    @NotBlank(message = "Имя не может быть пустым")
    @Size(max = 50, message = "Имя не может быть длиннее 50 символов")
    private String name;

    @NotBlank(message = "Email обязателен")
    @Email(message = "Некорректный формат email")
    private String email;

    @Min(value = 18, message = "Возраст должен быть не менее 18")
    @Max(value = 120, message = "Возраст не может быть больше 120")
    private Integer age;

    // Геттеры и сеттеры
}

3. Обновим резолвер для обработки мутации:


import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

@Component
@Validated
public class UserMutationResolver {

    public User createUser(@Valid CreateUserInput input) {
        // Логика создания пользователя
        User user = new User();
        user.setId(1); // Мокаем ID
        user.setName(input.getName());
        user.setEmail(input.getEmail());
        user.setAge(input.getAge());
        return user;
    }
}

4. Обрабатываем исключения: Если пользователь отправит некорректные данные, Spring автоматически сгенерирует исключение MethodArgumentNotValidException. Нам нужно обработать его и вернуть понятное сообщение на клиент.

Обработка ошибок:


import graphql.GraphQLError;
import graphql.kickstart.execution.error.GraphQLErrorHandler;
import graphql.language.SourceLocation;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class CustomGraphQLErrorHandler implements GraphQLErrorHandler {

    @Override
    public List<GraphQLError> processErrors(List<GraphQLError> errors) {
        return errors.stream()
                .map(error -> {
                    if (error.getMessage().contains("Validation failed")) {
                        return GraphQLError.newError()
                                .message("Валидация не пройдена: " + error.getMessage())
                                .location(new SourceLocation(error.getLocations().get(0).getLine(), error.getLocations().get(0).getColumn()))
                                .build();
                    }
                    return error;
                }).toList();
    }
}

Обработка ошибок в GraphQL

В случае любых ошибок, GraphQL возвращает структуру, содержащую как данные, так и массив ошибок. Например:


{
  "data": null,
  "errors": [
    {
      "message": "Валидация не пройдена: Имя не может быть пустым",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "extensions": {
        "code": "VALIDATION_ERROR"
      }
    }
  ]
}

Пример создания кастомных ошибок с GraphQLError

Мы можем создавать собственные ошибки для более точного контроля:


import graphql.GraphQLError;
import graphql.language.SourceLocation;

import java.util.List;

public class ValidationError implements GraphQLError {

    private final String message;

    public ValidationError(String message) {
        this.message = message;
    }

    @Override
    public String getMessage() {
        return message;
    }

    @Override
    public List<SourceLocation> getLocations() {
        return null; // Локацию можно оставить null, если она не критична
    }

    @Override
    public Map<String, Object> getExtensions() {
        return Map.of("code", "VALIDATION_ERROR");
    }
}

Чтобы не ловить каждую ошибку руками, мы можем определить обработчик типа @ControllerAdvice:


import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
import org.springframework.stereotype.Component;

@Component
public class GlobalExceptionHandler extends DataFetcherExceptionResolverAdapter {

    @Override
    protected GraphQLError resolveToSingleError(Throwable ex) {
        if (ex instanceof IllegalArgumentException) {
            return new ValidationError(ex.getMessage());
        }
        return null;
    }
}

Локализация сообщений об ошибках

Для международных приложений важно предоставлять сообщения об ошибках на языке пользователя. Spring предоставляет встроенную поддержку локализации через файлы messages.properties.

Настройка локализации:

1. Создайте property-файлы:

  • messages.properties (по умолчанию)
  • messages_ru.properties (для русского языка)

2. Пример содержимого файла:


name.notBlank=Имя не может быть пустым
email.invalid=Некорректный формат email

3. Настройте LocaleResolver:


import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.Locale;

@Configuration
public class LocaleConfig {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setBasename("messages");
        source.setDefaultEncoding("UTF-8");
        return source;
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver resolver = new SessionLocaleResolver();
        resolver.setDefaultLocale(Locale.ENGLISH);
        return resolver;
    }
}

Логирование ошибок

Важно не только обрабатывать ошибки для клиентов, но и логировать их для дальнейшего анализа. Используйте библиотеки, такие как SLF4J и Logback:


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class LoggingErrorHandler {

    private static final Logger logger = LoggerFactory.getLogger(LoggingErrorHandler.class);

    public void logError(Throwable ex) {
        logger.error("Произошла ошибка: {}", ex.getMessage(), ex);
    }
}

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

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ