---
name: env-module-getenv-import-order
description: "Модульный os.getenv молча берёт пустую строку, если load_dotenv импортируется позже — фича ЗБР лежала 6 недель, а в логе был только чужой 401"
metadata: 
  node_type: memory
  type: reference
  originSessionId: d0c6c5aa-4d05-42e2-a1f9-034482b88603
  modified: 2026-08-02T11:16:01.179Z
---

**2026-08-02, сайт «Земля без риска».** Поиск участка по адресу и по точке на карте отвечал пользователю 502 **шесть недель** — с 18 июня. В логе бота 47 ответов `401` на `/internal/resolve-cadastral`, и ни одной строки в логе сайта: он честно отдавал `lookup_failed`.

**Причина — порядок импортов.** `app/routes/lookup.py` брал `ZW_INTERNAL_TOKEN` через `os.getenv` **на уровне модуля**, а импортировался в `main.py` 24-й строкой. Единственный на весь бэкенд `load_dotenv` лежал в `app/routes/oauth.py` — 31-й. К моменту чтения переменной в окружении её ещё не было: токен пустой, заголовок не отправлен, бот отвечает 401 совершенно правильно.

**`pydantic-settings` не спасает.** `SettingsConfigDict(env_file=".env")` наполняет объект `Settings`, но **не** `os.environ`. Конфиг «есть», а `os.getenv` пуст.

**Лечение:** `load_dotenv(..., override=False)` в самом рано импортируемом модуле (у нас `app/config.py`), путь — абсолютный от `__file__`, а не от CWD. `override=False` оставляет launchd главнее файла.

**Диагностика, которая сработала:** сравнить токены в двух `.env` (совпали) → дёрнуть эндпоинт curl-ом с токеном (200) → проверить не значение, а **видимость на импорте**: `python -c "from app.routes import lookup; print(bool(lookup._INTERNAL_TOKEN))"`. `ps eww` на macOS чужой процесс не показывает — не тратить на него время.

**Правило:** модульный `os.getenv` в веб-приложении — мина: срабатывает один раз и молчит. Есть такой код — `.env` обязан грузиться раньше самого раннего из них. Проверять `grep -rn "^_\?[A-Z_]* *= *os.getenv" app/`.

См. [[env_source_defeats_test_setdefault]], [[deployed_is_not_running]], [[zemlya_web_overview]].
