[P0] tradein: ключ Яндекс-геокодера недействителен — 403 Invalid api key, работает только резервный Nominatim #2585

Closed
opened 2026-07-31 14:41:31 +00:00 by lekss361 · 1 comment
Owner

Обнаружено при deep-review PR #2580, подтверждено лично прямым запросом с прода.

Факт

GET https://geocode-maps.yandex.ru/1.x/?apikey=<ключ из YANDEX_GEOCODER_API_KEY>&geocode=Екатеринбург,+Малышева,+30
→ HTTP 403 {"statusCode":403,"error":"Forbidden","message":"Invalid api key"}

Ключ в окружении tradein-backend задан (36 символов), но недействителен. В логах приложения это видно как yandex suggest failed, falling back to nominatim + RetryError[HTTPStatusError 403].

Почему это важно

Яндекс — основной внешний геокодер. Сейчас каждый запрос к нему:

  1. отрабатывает три повторные попытки с экспоненциальной паузой (~3-4 секунды впустую),
  2. падает,
  3. уходит на резервный Nominatim — бесплатный сервис с заметно худшим покрытием российской адресации.

Пользователь при этом ничего не замечает: адрес геокодируется, оценка строится. Тихая деградация качества, которая длится неизвестно сколько.

Дополнительно это съедает бюджет геокодирования (estimate_geocode_budget_s = 12s): пустые повторы Яндекса плюс повторы Nominatim с паузами могут упереться в лимит, и тогда оценка вернётся с «адрес не геокодирован». Сегодня это маскируется кэшем — но после включения передачи города с фронта (#2584) ключ кэша меняется, кэш становится холодным, и маскировка исчезнет.

Что сделать

  1. Проверить ключ в кабинете Яндекса: не истёк ли, не исчерпана ли квота, не отвязан ли от сервиса Геокодера (у Яндекса ключи привязаны к конкретному API — ключ Карт не работает для Геокодера).
  2. Выписать новый / починить существующий, обновить YANDEX_GEOCODER_API_KEY в .env.runtime на VPS (ручной шаг — правила проекта не дают Claude трогать .env*).
  3. Добавить мониторинг: сейчас отказ основного провайдера не виден никому. Нужен алерт на долю ответов 4xx от геокодера либо на долю запросов, ушедших в резерв, — иначе следующая такая поломка снова будет тихой.

Связанное

Пока ключ мёртв, любые правки путей _yandex_lookup / _yandex_suggest (в том числе в #2580) в проде не исполняются — оценка их корректности носит теоретический характер. Это стоит учитывать при приёмке этапа B эпика #2576.

Обнаружено при deep-review PR #2580, подтверждено лично прямым запросом с прода. ## Факт ``` GET https://geocode-maps.yandex.ru/1.x/?apikey=<ключ из YANDEX_GEOCODER_API_KEY>&geocode=Екатеринбург,+Малышева,+30 → HTTP 403 {"statusCode":403,"error":"Forbidden","message":"Invalid api key"} ``` Ключ в окружении `tradein-backend` **задан** (36 символов), но недействителен. В логах приложения это видно как `yandex suggest failed, falling back to nominatim` + `RetryError[HTTPStatusError 403]`. ## Почему это важно Яндекс — основной внешний геокодер. Сейчас каждый запрос к нему: 1. отрабатывает три повторные попытки с экспоненциальной паузой (~3-4 секунды впустую), 2. падает, 3. уходит на резервный Nominatim — бесплатный сервис с заметно худшим покрытием российской адресации. Пользователь при этом ничего не замечает: адрес геокодируется, оценка строится. **Тихая деградация качества**, которая длится неизвестно сколько. Дополнительно это съедает бюджет геокодирования (`estimate_geocode_budget_s = 12s`): пустые повторы Яндекса плюс повторы Nominatim с паузами могут упереться в лимит, и тогда оценка вернётся с «адрес не геокодирован». Сегодня это маскируется кэшем — но после включения передачи города с фронта (#2584) ключ кэша меняется, кэш становится холодным, и маскировка исчезнет. ## Что сделать 1. **Проверить ключ в кабинете Яндекса**: не истёк ли, не исчерпана ли квота, не отвязан ли от сервиса Геокодера (у Яндекса ключи привязаны к конкретному API — ключ Карт не работает для Геокодера). 2. Выписать новый / починить существующий, обновить `YANDEX_GEOCODER_API_KEY` в `.env.runtime` на VPS (ручной шаг — правила проекта не дают Claude трогать `.env*`). 3. **Добавить мониторинг**: сейчас отказ основного провайдера не виден никому. Нужен алерт на долю ответов 4xx от геокодера либо на долю запросов, ушедших в резерв, — иначе следующая такая поломка снова будет тихой. ## Связанное Пока ключ мёртв, любые правки путей `_yandex_lookup` / `_yandex_suggest` (в том числе в #2580) в проде **не исполняются** — оценка их корректности носит теоретический характер. Это стоит учитывать при приёмке этапа B эпика #2576.
Collaborator

Задача просила цену простоя числом. Цена оказалась нулевой, и по двум независимым причинам сразу — обе проверены сегодня, 2026-08-10 09:20-09:40 UTC.

1. Ключ больше не 403. Живой запрос из контейнера, код ответа своими глазами

docker exec tradein-backend curl -sG --data-urlencode "apikey=$YANDEX_GEOCODER_API_KEY" \
  --data-urlencode "format=json" --data-urlencode "geocode=Екатеринбург, Малышева, 30" \
  https://geocode-maps.yandex.ru/1.x/
→ HTTP 200, 2151 байт
{"response":{"GeoObjectCollection":{...,"found":"1"},"featureMember":[{"GeoObject":{...
 "precision":"exact","text":"Россия, Свердловская область, Екатеринбург, улица Малышева, 30","kind":"house"

Ключ тот же самый: длина 36 символов, /opt/gendesign/tradein-mvp/.env.runtime не менялся с 2026-07-04 12:52 MSK — то есть с момента заведения задачи переменную никто не трогал, а 403 пропал сам (услуга включена/квота восстановлена на стороне Яндекса).

Методическая оговорка — ровно тот случай, ради которого просили «своими глазами». Первая попытка, с кириллицей в URL без экранирования, вернула HTTP 400 с пустым телом. Если бы я остановился на ней, то отчитался бы «ключ по-прежнему не работает, только код другой». 200 даёт только запрос с --data-urlencode. Один запрос на проверку, второй на подтверждение — платный сервис не нагружался.

2. Платить всё равно нечем: провайдера в коде нет с 31 июля

Владелец 31.07 решил ключ не перевыпускать, и Яндекс-геокодер вырезан — #2593 (части #2595/#2596/#2599, −845 строк), issue закрыт 05.08. Проверено в живом прод-контейнере, а не в main:

docker exec tradein-backend grep -c "_yandex_lookup\|_yandex_suggest\|geocode-maps.yandex" app/services/geocoder.py
→ 0
grep -n "provider: Literal" app/services/geocoder.py
→ 39:    provider: Literal["nominatim", "cache"]

Ни трёх повторов с экспоненциальной паузой, ни «~3-4 секунды впустую», ни расхода бюджета estimate_geocode_budget_s на мёртвого провайдера — этого пути в проде физически нет. Единственный остаток — неиспользуемая переменная окружения (extra="ignore" в настройках).

Итого по пунктам задачи: п.1 отвечен — ключ действителен, в кабинет идти не нужно; п.2 отменён решением владельца; п.3 (алерт на долю 4xx геокодера / долю ухода в резерв) в прежней формулировке невыразим — «основного» и «резервного» больше нет, провайдер один. Закрываю.

Опровергнутая предпосылка: yandex_address_backfill к геокодеру отношения не имеет

Замер saved: 1, errors: 21, checked: 200, skipped: 178 (прогон 3586, 10.08 03:00) читался как цена мёртвого геокодера. Это не так. app/services/yandex_address_backfill.py — скрейпер: тянет detail-страницы Яндекс.Недвижимости через curl_cffi и достаёт номер дома из <title> регуляркой. Ни одного вызова геокодера, ключ не читается вовсе. skipped: 178 — это карточки, у которых номер дома уже есть. Ряд прогонов errors: 15 без единого исключения с 16.07 по 03.08 — ровно та же полка, что и до 403, и после удаления геокодера: величина от ключа не зависела никогда.

Что от геокодирования болит на самом деле — с числом

Цена, которую стоит нести владельцу, лежит не в ключе:

3 270 активных объявлений без координат (все — Авито, 34.9% его активного инвентаря), 706 из 9 179 домов без координат (7.7%). Из них 2 446 попадают в свежий comp-пул эстиматора (is_active, scraped_at < 30 сут, сегмент вторичка/NULL) — то есть 12.7% аналогов невидимы радиусному поиску, потому что Tier W отбирает через ST_DWithin(geom, …), а geom IS NULL выпадает молча.

И корень у трети из них — не геокодер, а парсер адреса Авито:

класс адреса без координат из них пробовали геокодить
«загрязнён» (ул. Ткачей,17·5,0 · 4 отзыва) 1 123 711
пустой 360 0
без номера дома 832 816
чистый (ждёт очереди) 949 381

У загрязнённых 1 123 из 1 123 без координат — доля 100%. Для сравнения: у чистых адресов координаты есть у 4 766. То есть в listings.address с 27.07 подклеивается рейтинг и число отзывов из карточки Авито, и такой адрес не берёт ни один геокодер. Завожу отдельной задачей — здесь это уже другой дефект.

Задача просила цену простоя числом. Цена оказалась **нулевой, и по двум независимым причинам сразу** — обе проверены сегодня, 2026-08-10 09:20-09:40 UTC. ## 1. Ключ больше не 403. Живой запрос из контейнера, код ответа своими глазами ``` docker exec tradein-backend curl -sG --data-urlencode "apikey=$YANDEX_GEOCODER_API_KEY" \ --data-urlencode "format=json" --data-urlencode "geocode=Екатеринбург, Малышева, 30" \ https://geocode-maps.yandex.ru/1.x/ → HTTP 200, 2151 байт {"response":{"GeoObjectCollection":{...,"found":"1"},"featureMember":[{"GeoObject":{... "precision":"exact","text":"Россия, Свердловская область, Екатеринбург, улица Малышева, 30","kind":"house" ``` Ключ тот же самый: длина 36 символов, `/opt/gendesign/tradein-mvp/.env.runtime` не менялся с **2026-07-04 12:52 MSK** — то есть с момента заведения задачи переменную никто не трогал, а 403 пропал сам (услуга включена/квота восстановлена на стороне Яндекса). **Методическая оговорка — ровно тот случай, ради которого просили «своими глазами».** Первая попытка, с кириллицей в URL без экранирования, вернула **HTTP 400 с пустым телом**. Если бы я остановился на ней, то отчитался бы «ключ по-прежнему не работает, только код другой». 200 даёт только запрос с `--data-urlencode`. Один запрос на проверку, второй на подтверждение — платный сервис не нагружался. ## 2. Платить всё равно нечем: провайдера в коде нет с 31 июля Владелец 31.07 решил ключ **не перевыпускать**, и Яндекс-геокодер вырезан — #2593 (части #2595/#2596/#2599, −845 строк), issue закрыт 05.08. Проверено в **живом прод-контейнере**, а не в main: ``` docker exec tradein-backend grep -c "_yandex_lookup\|_yandex_suggest\|geocode-maps.yandex" app/services/geocoder.py → 0 grep -n "provider: Literal" app/services/geocoder.py → 39: provider: Literal["nominatim", "cache"] ``` Ни трёх повторов с экспоненциальной паузой, ни «~3-4 секунды впустую», ни расхода бюджета `estimate_geocode_budget_s` на мёртвого провайдера — этого пути в проде физически нет. Единственный остаток — неиспользуемая переменная окружения (`extra="ignore"` в настройках). Итого по пунктам задачи: **п.1** отвечен — ключ действителен, в кабинет идти не нужно; **п.2** отменён решением владельца; **п.3** (алерт на долю 4xx геокодера / долю ухода в резерв) в прежней формулировке невыразим — «основного» и «резервного» больше нет, провайдер один. Закрываю. ## Опровергнутая предпосылка: `yandex_address_backfill` к геокодеру отношения не имеет Замер `saved: 1, errors: 21, checked: 200, skipped: 178` (прогон 3586, 10.08 03:00) читался как цена мёртвого геокодера. Это не так. `app/services/yandex_address_backfill.py` — скрейпер: тянет detail-страницы Яндекс.Недвижимости через `curl_cffi` и достаёт номер дома из `<title>` регуляркой. Ни одного вызова геокодера, ключ не читается вовсе. `skipped: 178` — это карточки, у которых номер дома уже есть. Ряд прогонов `errors: 15` без единого исключения с 16.07 по 03.08 — ровно та же полка, что и до 403, и после удаления геокодера: величина от ключа не зависела никогда. ## Что от геокодирования болит на самом деле — с числом Цена, которую стоит нести владельцу, лежит не в ключе: **3 270 активных объявлений без координат** (все — Авито, 34.9% его активного инвентаря), **706 из 9 179 домов** без координат (7.7%). Из них **2 446 попадают в свежий comp-пул эстиматора** (`is_active`, `scraped_at` < 30 сут, сегмент вторичка/NULL) — то есть **12.7% аналогов невидимы радиусному поиску**, потому что Tier W отбирает через `ST_DWithin(geom, …)`, а `geom IS NULL` выпадает молча. И корень у трети из них — **не геокодер, а парсер адреса Авито**: | класс адреса | без координат | из них пробовали геокодить | |---|---|---| | «загрязнён» (`ул. Ткачей,17·5,0 · 4 отзыва`) | **1 123** | 711 | | пустой | 360 | 0 | | без номера дома | 832 | 816 | | чистый (ждёт очереди) | 949 | 381 | **У загрязнённых 1 123 из 1 123 без координат — доля 100%.** Для сравнения: у чистых адресов координаты есть у 4 766. То есть в `listings.address` с 27.07 подклеивается рейтинг и число отзывов из карточки Авито, и такой адрес не берёт ни один геокодер. Завожу отдельной задачей — здесь это уже другой дефект.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2585
No description provided.