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

Лекція 289: Валідація та обробка помилок у GraphQL

Модуль 5. Spring
Рівень 15 , Лекція 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, роблячи його більш надійним і зручним для взаємодії з клієнтами та розробниками.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ