Грабли Beget API
Поведение, которое нельзя вывести из документации и которое уже стоило потерянных DNS-записей. Всё ниже проверено живыми вызовами на боевом аккаунте.
Ошибка метода приезжает в success-конверте
api.beget.com отвечает HTTP 200 и {"status": "success", "answer": {...}}, а
собственную ошибку метода кладёт внутрь answer:
{"status": "success",
"answer": {"status": "error",
"errors": [{"error_code": "METHOD_FAILED", "error_text": "Failed to get DNS records"}]}}
Проверять только конверт недостаточно. Пока BegetClient.call() возвращал такой
answer как значение, ломались сразу две вещи:
- любой
except BegetAPIErrorстановился недостижим — в частности фолбэкdns_getна родительскую зону никогда не срабатывал; _get_result()отдавал{}, read-merge-write считал зону пустой иdns/changeRecordsстирал все записи, которых не было в запросе.
Отсюда два инварианта, закрытых тестами:
client.call()райзитBegetAPIError, еслиanswer.status == "error"(tests/test_client.py);_get_result()райзитNO_ZONE_DATA, если зона не прочиталась, и запись не выполняется (tests/test_dns.py).
Второй инвариант нужен отдельно от первого: фейковый клиент в тестах райзил корректно, поэтому тесты фолбэка проходили, пока живой клиент молчал. Дыра была ровно на границе клиента.
www-алиасы поддоменов не существуют для API
Beget автоматически заводит www.sub.site.ru к поддомену sub.site.ru, и в
панели такой алиас виден. Для API его нет:
domain/getSubdomainListего не возвращает (проверено: 20 реальных поддоменов, ни одногоwww.*);dns/getDataпо нему отвечаетMETHOD_FAILED.
dns_get в этом случае уходит на родительскую зону и помечает ответ полями
note, queried_fqdn, parent_zone. Тот же фолбэк закрывает DKIM-имена вида
mail._domainkey.site.ru, которые тоже иногда живут на уровне родителя.
Фолбэк срабатывает только на METHOD_FAILED. INVALID_DATA, лимиты и
транзиентные сбои отдаются как есть: иначе они подменялись бы записями родителя
и пометкой «поддомена не существует», и настоящая причина терялась бы.
Setter-тулам зоны нет — они райзят SUBDOMAIN_NOT_CREATED, если родительская
зона при этом читается (лечится domain_add_subdomain), и NO_ZONE_DATA в
остальных случаях. Вслепую не пишут ни в том, ни в другом. Осознанно записать
зону целиком, не читая её, можно через dns_set_records с replace_all=True —
этот путь идёт мимо read-merge-write, и для несозданного поддомена он НЕ помогает:
сущность так не появляется.
Зачем это разделение, чем опасны автозаписи нового поддомена, почему отсутствие
записи в зоне не означает «имя не резолвится» и почему success от dns_set_*
не равен «отдаётся миру» — docs/dns-facts-vs-intent.md.
Имена параметров, которые легко угадать неверно
Проверено живыми вызовами, ошибочные варианты возвращают INVALID_DATA:
| Метод | Верно | Неверно |
|---|---|---|
cron/delete |
row_number (поле так и называется в cron/getList) |
id |
mysql/changeAccessPassword |
suffix + access + password; access обязателен |
без access |
stat/getSiteLoad |
site_id — принимает и число, и строку |
— |
Пароль API: спецсимволы и query string
Пароль аккаунта Beget API может содержать # и прочие символы, ломающие URL.
BegetClient отправляет login/passwd POST’ом в теле формы, поэтому не
задет: кодирование берёт на себя requests, и пароль не попадает в access-логи
по пути. В query остаётся только output_format.
Сторонние клиенты этим не защищены: dns_beget.sh из acme.sh кладёт креды прямо
в query string без URL-кодирования (dnsapi/dns_beget.sh), и на # в пароле
curl обрывает URL — наружу это выглядит как error code: 3 /
«Can’t get domain list».