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

Таксономия ошибок

Все ошибки config-stack'а — это ConfigError с семью нормативно зафиксированными reason-значениями. Структура одинакова во всех реализациях:

ConfigError {
path: string # dot-notation (database.host)
reason: ConfigErrorReason # enum
details: string # human-readable message
source_id?: string # yaml:/app-config.yaml, etc.
}

Полный enum ConfigErrorReason

Нормативный источник — config-spec/_meta/error_reasons.yaml.

missing

Путь не существует в конфиге, и default не передан в getter.

config.get_string("nonexistent.path")
# ConfigError(
# path="nonexistent.path",
# reason="missing",
# details="Key 'nonexistent.path' not found in config and no default provided",
# )

Как обрабатывать: проверьте орфографию пути, добавьте default-аргумент или убедитесь, что ключ есть в app-config.yaml.

type_mismatch

Значение существует, но несовместимо с запрашиваемым типом.

# YAML: pool_size: "twenty"
config.get_int("database.pool_size")
# ConfigError(
# path="database.pool_size",
# reason="type_mismatch",
# details="Expected int, got string 'twenty' (does not match ^-?\\d+$)",
# )

Как обрабатывать: исправьте значение в YAML на корректное для ожидаемого типа. Интерполированные env-значения всегда строки — убедитесь, что строковое представление соответствует регулярному выражению типа:

  • int: ^-?\d+$
  • float: стандартный парсер чисел с плавающей точкой
  • bool: true|false|yes|no|1|0 (case-insensitive)

env_unresolved

Env-переменная в ${VAR} не задана, и default не указан.

# app-config.yaml:
database:
password: "${DB_PASSWORD}"

Если DB_PASSWORD не задана:

ConfigError(
path="database.password",
reason="env_unresolved",
details="Environment variable DB_PASSWORD is not set and no default provided",
)

Как обрабатывать: установите env-переменную (export DB_PASSWORD=...), или — только для не-секретных значений — добавьте значение по умолчанию в YAML (${LOG_LEVEL:-INFO}). Для секретов без значения по умолчанию это защита: отсутствие env сразу вызывает явную ошибку на старте.

validation_failed

Получение типизированной секции (get_section / getSection / GetSection) не прошло валидацию schema.

class DatabaseConfig(BaseModel):
host: str
password: str = Field(..., min_length=1)

# YAML: password: "" (пустая строка)
config.get_section("database", DatabaseConfig)
# ConfigError(
# path="database.password",
# reason="validation_failed",
# details="String should have at least 1 character",
# )

Как обрабатывать: исправьте значение в YAML; перед деплоем добавьте staging-проверку Config.load() + get_section / getSection / GetSection для всех секций.

parse_error

YAML-файл не парсится (синтаксическая ошибка).

# Невалидный YAML:
database:
host: "localhost
port: 5432
ConfigError(
path="",
reason="parse_error",
details="YAML parse error at line 3: unexpected end of stream",
source_id="yaml:/app/app-config.yaml",
)

Как обрабатывать: проверьте YAML через yamllint или yq, исправьте синтаксис. IDE с YAML-lint показывает ошибки сразу.

source_unavailable

ConfigSource не смог загрузить данные.

# Файл не существует:
Config.load("non-existent.yaml")
# ConfigError(
# path="",
# reason="source_unavailable",
# details="File '/app/non-existent.yaml' does not exist",
# source_id="yaml:/app/non-existent.yaml",
# )

Другие примеры для будущих источников Phase 2+:

  • EtcdSource: connection timeout → source_unavailable.
  • VaultSource: auth-fail → source_unavailable.
  • HttpSource: 5xx → source_unavailable.

Как обрабатывать: проверьте существование файла / доступность сервиса / корректность учётных данных.

reload_rejected

Hot-reload не применён из-за ошибки валидации в новой версии конфига.

ConfigError(
path="database.pool_size",
reason="reload_rejected",
details="New config rejected: database.pool_size expected number, got string 'invalid'",
)

Это логируется, не выбрасывается — операция перезагрузки откатывается атомарно, подписчики не получают уведомления, приложение продолжает работать со старыми значениями. Диагностический warning в лог — для оператора.

Иерархия (в реализациях)

from dagstack.config import ConfigError, ConfigErrorReason

try:
config = Config.load("app-config.yaml")
db_cfg = config.get_section("database", DatabaseConfig)
except ConfigError as exc:
match exc.reason:
case ConfigErrorReason.missing:
log.error(f"Конфиг не содержит {exc.path}")
case ConfigErrorReason.env_unresolved:
log.error(f"Env-переменная для {exc.path} не задана")
case ConfigErrorReason.validation_failed:
log.error(f"Невалидный конфиг {exc.path}: {exc.details}")
case _:
log.error(f"Config-ошибка: {exc}")
raise

Маскирование секретов в сообщениях

Поля из scrub-list (api_key, *_token, *_password, *_secret, ...) автоматически маскируются в details:

ConfigError(
path="database.password",
reason="validation_failed",
details="Password '[MASKED]' does not match required pattern", # реальное значение не раскрыто
)

См. Секреты.

См. также