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

Ошибки REST API: коды ответа, формат и повторы

REST API отвечает стандартными HTTP-кодами. Успешный ответ — 200 с телом в формате эндпоинта; всё остальное описано ниже.

Ошибки самих сервисов — проверка параметров, сбой источника данных — приходят в формате problem+json с Content-Type: application/problem+json:

{
"title": "Unprocessable Entity",
"status": 422,
"detail": "validation failed",
"errors": [
{
"message": "expected required property url to be present",
"location": "body",
"value": {}
}
]
}
Поле Описание
status HTTP-код, продублированный в теле
title стандартное название кода
detail что произошло
errors для 422 — список нарушений: location указывает на параметр (body.url, query.domain), message — на правило

Сервисы браузера отвечают на сбой загрузки страницы своим JSON — с полями error, errorKind и code.

Ошибки доступа (401, 403), нечитаемое тело пакетного запроса (400), отказ по размеру пакета (413), внутренний сбой проверки ключа или баланса (500) и недоступность проверки ключа или прайса (503) приходят коротким текстом (text/plain) — их удобно логировать как есть. 402 приходит JSON-объектом, см. ниже.

Код Что означает Что делать
400 тело запроса не читается как JSON или параметр недопустим (например, больше 100 адресов в пакете IP) исправить запрос
401 нет ключа, неверный формат заголовка, ключ неизвестен, отозван или истёк проверить ключ, см. Аутентификация
402 на балансе не хватает средств на вызов пополнить баланс на seyka.ru
403 аккаунт заблокирован; у Wordstat и Keywords — исчерпана квота Яндекса при блокировке — написать в поддержку; квота — повторить позже
404 ресурс не найден (отложенный пакет поиска, адрес в базе IP) проверить идентификатор
413 в пакете поиска, Redirekto или Tavily Extract больше элементов, чем допускает эндпоинт, или тело больше 1 МБ разбить пакет
422 параметры не прошли проверку схемы исправить запрос по полю errors
429 исчерпан лимит запросов к Яндексу (поиск, Wordstat) повторить позже, с паузой
500 внутренний сбой сервиса или проверки баланса повторить позже, с паузой
502 источник данных не ответил или ответил ошибкой повторить позже; если повторяется — проверить входные данные
503 проверка ключа, прайс или сам сервис временно недоступны повторить позже, с паузой
504 браузерный сервис не дождался страницы (таймаут загрузки или ожидания) повторить позже, с паузой

Стоимость вызова проверяется до его выполнения: если баланса не хватает, запрос не выполняется и ничего не списывается. Исключение — умный поиск и совместимые с Brave и Tavily эндпоинты: они проверяют баланс по цене ответа из кэша, а списывают по ступени, которая ответила, поэтому баланс может уйти в минус.

{
"error": "insufficient_balance",
"required": 12,
"balance": 3
}

required и balance — в копейках. Для пакетного запроса required — цена за элемент, умноженная на число оплачиваемых элементов: в поиске это выборки (запрос больше 100 результатов — две), в Tavily Extract — разные URL. Цены — в таблице цен.

502 означает, что запрос дошёл до сервиса, но источник данных — поисковая система, удалённый сайт, реестр — не дал результата. Для Redirekto и модерации так же выглядит отказ проверять адрес во внутренней сети и вход, который источник счёл некорректным: тело источника наружу не передаётся.

Некоторые сервисы отвечают 200, даже когда целевой документ получить не удалось: сервис отработал, а результат — диагностика. Smart Scrape в этом случае возвращает ok: false и message, проверка в реестре РКН — поле error. Проверяйте эти поля, а не только HTTP-код. Такой ответ оплачивается как успешный — кроме Smart Scrape, если не успел выполниться ни один платный шаг.

Повторять безопасно 429, 500, 502, 503 и 504 — с экспоненциальной паузой (например, 1, 2, 4, 8 секунд) и ограниченным числом попыток. 400, 401, 413, 422 и 403 из-за блокировки аккаунта повтор не исправит; 403 из-за квоты Wordstat пройдёт позже: нужно менять запрос или ключ. 402 пройдёт после пополнения баланса.

async function withRetry(call: () => Promise<Response>, attempts = 4): Promise<Response> {
for (let i = 0; ; i++) {
const response = await call();
if (![429, 500, 502, 503, 504].includes(response.status) || i === attempts - 1) return response;
await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** i));
}
}