Skip to content

AleksandrGurkin/xerror

Repository files navigation

xerror

CI

Библиотека структурированных ошибок: иммутабельные sentinel-ошибки, обёртка с sentinel, контрактные ошибки с локализацией и кодогенерация из YAML-каталога.

Что такое Sentinel-ошибка?

Паттерн sentinel error (ошибка-страж) — это универсальная архитектурная концепция, при которой заранее определённые константы используются для обозначения конкретных бизнес-ошибок или состояний (например, «Пользователь не найден» или «Недостаточно средств»).

Вместо парсинга текста ошибки, принимающая сторона (на сервере или клиенте) просто сравнивает полученный код/экземпляр ошибки с известным списком (sentinel-значениями) и ветвит логику. Это делает обработку отказов предсказуемой и легко тестируемой на любом языке программирования. В Go для этого используется errors.Is, в Dart — сравнение строковых кодов или константных классов.

Установка

go get github.com/AleksandrGurkin/xerror

Возможности

Библиотека обеспечивает базовые строительные блоки для трёхтиповой модели ошибок: доменные, контрактные и коммуникационные.

Компонент Описание
Const Иммутабельные константные ошибки, объявляемые на уровне пакета
WrapWithSentinel Создаёт обёртку, связывающую ошибку зависимости с доменным sentinel. errors.Is работает для обоих
WithDetails / DetailsFromError Прикрепляет контекстные детали к ошибке; детали путешествуют по цепочке и автоматически мержатся в ContractError
CommunicationError Обёртка с хелперами WrapTimeout / WrapConnection и fluent API ( WithReason, WithRetryable) для обработки инфраструктурных сбоев
ContractError Контрактная ошибка: код, HTTP/gRPC статус, Localize() для перевода сообщений на разные языки, WithDetails() с фильтрацией по публичности, WithDetailsFrom() для извлечения деталей из цепочки
httperr Пакет для удобной записи и парсинга ContractError в HTTP JSON ответы ( WriteError, ParseResponse, LocalizationMiddleware )
grpcerr Пакет для автоматической трансформации ContractError в status.Status с errdetails.ErrorInfo (включая Server Interceptor)
catalog YAML-каталог + парсер + валидация (формат кода, домен, HTTP-статус, метаданные)
xerrorgen CLI-генератор Go и Dart кода из YAML-каталога

Использование

Const — константные ошибки

const ErrNotFound = xerror.Const("NOT_FOUND")

func doSomething() error {
    return ErrNotFound
}

WrapWithSentinel — обёртка с sentinel

WrapWithSentinel создаёт обёртку, связывающую оригинальную ошибку с доменным sentinel. Типичное применение — трансформация контрактной ошибки зависимости в доменный sentinel.

// Доменные sentinel-ы определяются как константы:
const (
    ErrUserNotFound   = xerror.Const("USER_NOT_FOUND")
    ErrNotEnoughMoney = xerror.Const("NOT_ENOUGH_MONEY")
)

// Инфраструктурный слой: контрактная ошибка зависимости → доменный sentinel
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    user, err := r.db.Get(ctx, id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, xerror.WrapWithSentinel(err, ErrUserNotFound)
    }
    return user, err
}

Результат WrapWithSentinel — скрытый тип-обёртка (реализует error), который поддерживает:

  • Is(target) — сравнивает с sentinel: errors.Is(err, ErrUserNotFound)true
  • Unwrap() — возвращает оригинал: errors.Is(err, sql.ErrNoRows)true
  • Error()"USER_NOT_FOUND: sql: no rows in result set"
// Обе проверки работают:
errors.Is(err, ErrUserNotFound) // true — через кастомный Is()
errors.Is(err, sql.ErrNoRows)  // true — через Unwrap()-цепочку

Communication Error — инфраструктурные сбои

Позволяет обернуть неожиданные или инфраструктурные ошибки (например, таймауты) с добавлением контекста, причины сбоя (Reason) и флагом возможности повтора (Retryable). Поддерживается fluent API и готовые хелперы.

// Сбой связи с базой данных (хелпер сам поставит ReasonTimeout и Retryable = true)
err := doNetworkRequest()
if errors.Is(err, context.DeadlineExceeded) {
    return xerror.WrapTimeout(err, "failed to query %s", "users table")
}

// Fluent API для нестандартных случаев (Внимание: используйте if err != nil, чтобы избежать typed nil)
if err != nil {
    return xerror.WrapCommunication(err, "custom error").
        WithReason(xerror.ReasonProtocol).
        WithRetryable(false)
}

// Позже, на верхнем уровне:
type retryer interface{ Retryable() bool }
if r, ok := err.(retryer); ok && r.Retryable() {
    // Начать exponential backoff...
}

ContractError — контрактные ошибки

Ошибки, пересекающие границу сервиса. Генерируются из YAML-каталога через xerrorgen:

// Сгенерировано xerrorgen:
var AuthUserNotFound = xerror.ContractError{
    Code:       "AUTH_USER_NOT_FOUND",
    HTTPStatus: 404,
    GRPCCode:   "NOT_FOUND",
    Public:     true,
    Message:    "User not found",
    Messages:   map[string]string{"en": "User not found", "ru": "Пользователь не найден"},
}

ЛокализацияLocalize выбирает сообщение по языку, по умолчанию "en":

resp := apperrors.AuthUserNotFound.Localize("ru")
// resp.Message == "Пользователь не найден"

ДеталиWithDetails добавляет контекст. Для публичных ошибок фильтрует по whitelist из YAML metadata:

resp := apperrors.BillingInsufficientFunds.
    Localize("ru").
    WithDetails(map[string]any{
        "balance":  50.0,   // ✓ в AllowedDetails → пропустит
        "user_id":  "abc",  // ✗ не в AllowedDetails → отфильтрует
    })

Детали из цепочки ошибокxerror.WithDetails прикрепляет контекст к ошибке на уровне домена. Детали путешествуют по цепочке и автоматически подхватываются WriteError / gRPC interceptor:

// В доменном сервисе — прикрепляем детали там, где есть контекст:
return xerror.WithDetails(
    domain.ErrInsufficientFunds,
    map[string]any{"balance": balance},
)

// В handler-е — WithDetailsFrom извлекает детали из цепочки и мержит в ContractError:
if errors.Is(err, domain.ErrInsufficientFunds) {
    httperr.WriteError(ctx, w, apperrors.BillingInsufficientFunds.WithDetailsFrom(err))
}

HTTP и gRPC Транспорты (httperr, grpcerr)

Для удобной передачи контрактных ошибок по сети библиотека предоставляет готовые адаптеры:

HTTP REST (xerror/httperr)

import "github.com/AleksandrGurkin/xerror/httperr"

// На сервере: подключаем LocalizationMiddleware (аналог grpcerr.UnaryServerInterceptor)
mux := http.NewServeMux()
mux.Handle("/orders", orderHandler)
server := http.Server{Handler: httperr.LocalizationMiddleware(mux)}

// В Handler-е: WriteError принимает ctx, а не *http.Request
func myHandler(w http.ResponseWriter, r *http.Request) {
    err := doSomething()
    httperr.WriteError(r.Context(), w, err) // Локализация через middleware автоматически
}

// На клиенте (HTTP Client):
resp, _ := httpClient.Do(req)
contractErr, parseErr := httperr.ParseResponse(resp)
if contractErr != nil {
    fmt.Println(contractErr.Message) // Распакованный JSON -> ContractError
}

gRPC (xerror/grpcerr) Использует механизм Rich Errors, прикрепляя оригинальный код, статус transient и детали в errdetails.ErrorInfo.

import "github.com/AleksandrGurkin/xerror/grpcerr"

// На сервере достаточно подключить интерсептор:
server := grpc.NewServer(
    grpc.UnaryInterceptor(grpcerr.UnaryServerInterceptor()),
)

// На клиенте:
resp, err := grpcClient.Charge(ctx, req)
if err != nil {
    contractErr, ok := grpcerr.FromError(err)
    if ok {
        fmt.Println(contractErr.Message) // Распакованный статус -> ContractError
    }
}

Warning

Type Loss в gRPC Details При передаче ContractError.Details через gRPC все значения сериализуются в строки (map[string]string), так как это ограничение формата gRPC Metadata. Если вы отправляли числа (123) или булевые значения (true), на клиенте в Details они будут десериализованы как строки ("123", "true"). Будьте внимательны при написании клиентской логики.

Каталог ошибок (catalog)

YAML-файл — единый источник правды для контрактных ошибок:

errors:
  AUTH_USER_NOT_FOUND:
    domain: auth
    grpc_code: NOT_FOUND
    http_status: 404
    transient: false
    public: true
    messages:
      ru: "Пользователь не найден"
      en: "User not found"
    description: "Returned when user ID does not exist"
    metadata:
      - name: retry_after
        type: int

Парсинг и валидация:

import "github.com/AleksandrGurkin/xerror/catalog"

c, err := catalog.ParseFile("errors/catalog.yaml")

Валидация проверяет: формат кода (UPPER_SNAKE_CASE), совпадение домена с префиксом, HTTP-статус 400–599, наличие сообщений, типы метаданных.

Note

metadataAllowedDetails: Поля из секции metadata в YAML-каталоге превращаются в AllowedDetails в сгенерированном Go-коде. Для публичных ошибок (public: true) это белый список — при вызове WithDetails() только ключи из этого списка попадут в ответ клиенту. Для непубличных ошибок все детали передаются без фильтрации.

xerrorgen — кодогенерация

CLI-инструмент для генерации Go и Dart кода из YAML-каталога:

# Go: pre-built ContractError instances
go run ./cmd/xerrorgen -lang=go -input=catalog.yaml -output=errors_gen.go -package=apperrors

# Dart: ErrorCodes, transientErrors, publicErrors, grpcCodes, fallbackMessages
go run ./cmd/xerrorgen -lang=dart -input=catalog.yaml -output=errors_gen.dart

Примеры

В репозитории доступны полноценные примеры микросервисного взаимодействия с перехватом, пробросом и трансформацией ошибок между сервисами разного уровня:

Документация

  • Классификация ошибок — трёхтиповая модель (контрактные, доменные, коммуникационные), трансформация между уровнями, каталогизация

Лицензия

См. файл LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages