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

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

Любая ошибка из стека конфигурации — это ConfigError с одним из семи нормативно зафиксированных значений reason. Структура одинакова во всех биндингах:

ConfigError {
path: string # точечная нотация (database.host)
reason: ConfigErrorReason # enum
details: string # человекочитаемое сообщение
source_id?: string # yaml:/app-config.yaml и так далее
}

Полный enum ConfigErrorReason

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

missing

Путь не существует в конфиге, и геттер не получил значение по умолчанию.

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 (без учёта регистра)

env_unresolved

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

# 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=...) или — только для несекретных значений — добавь default в YAML (${LOG_LEVEL:-INFO}). Для секретов без default это страховка: пропущенная env-переменная даёт явную ошибку при старте.

validation_failed

Чтение типизированной секции (get_section / getSection / GetSection) не прошло валидацию по схеме.

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",
)

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

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: тайм-аут подключения → source_unavailable.
  • VaultSource: сбой аутентификации → source_unavailable.
  • HttpSource: 5xx → source_unavailable.

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

reload_rejected

Горячая перезагрузка не была применена, потому что валидация на новой версии конфига провалилась.

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

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

Иерархия в каждом биндинге

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"Config does not contain {exc.path}")
case ConfigErrorReason.env_unresolved:
log.error(f"Env variable for {exc.path} is not set")
case ConfigErrorReason.validation_failed:
log.error(f"Invalid config at {exc.path}: {exc.details}")
case _:
log.error(f"Config error: {exc}")
raise

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

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

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

См. Секреты.

См. также