Ошибки API
Обработка ошибок в FourA API.
Формат ответа с ошибкой
API возвращает плоские JSON объекты для всех ошибок. Вложенный объект error отсутствует. Если у ошибки есть машиночитаемый код, он передается в поле верхнего уровня: reason при превышении лимита тарифа, code при proxy вызове без подходящего узла выхода.
{
"error": "Invalid API key"
}
Некоторые ошибки содержат дополнительные поля на верхнем уровне, такие как status, service, retryAfter, current или limits:
{
"error": "Rate limit exceeded",
"status": 429,
"service": "single",
"retryAfter": 5,
"current": { "concurrency": 12, "rpm": 3000 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
Отслеживание запроса
Каждый API response (успешный или с ошибкой) содержит header X-FourA-Request-Id с UUID для этого вызова, за исключением случаев, когда FourA не может прочитать body (некорректный JSON или body более 100 KB): такие запросы отклоняются до назначения ID. Логируйте его на своей стороне. Если вам потребуется уточнить у поддержки статус конкретного request, этот ID позволит нам найти его.
curl -i -X POST https://eu.api.foura.ai/api/single/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"method": "GET", "url": "https://example.com"}'
# HTTP/1.1 200 OK
# X-FourA-Request-Id: 9f1c4e6c-7b2a-4d3e-8a1f-2c9d8e4a3b15
# Content-Type: application/json
# ...
Типы ошибок
400: Bad Request
Тело request не содержит обязательных полей, содержит недопустимые значения или указывает целевой ресурс, к которому API отказывается выполнять запрос.
{
"error": "Invalid request body format"
}
Этот же код 400 охватывает и защиту от SSRF. Если ваш url резолвится в приватный, loopback или иной зарезервированный диапазон IP (RFC 5735, RFC 6598, зарезервированные блоки IPv6), request отклоняется до того, как покинет сеть FourA:
{
"error": "Refusing to fetch <target>: target resolves to a private or reserved IP range. The FourA scraping API only forwards requests to public internet hosts."
}
<target> представляет собой адрес либо имя хоста и адрес, в который оно было разрешено. URL, который не удается распарсить или который не является http:// или https://, возвращает ту же ошибку 400.
Имя хоста, которое не удается разрешить через DNS, не отклоняется сразу. Вызов возвращает HTTP 200 с status: 0 и причиной (could not resolve <host>: <reason>), как и любой недоступный для FourA целевой ресурс, и не тарифицируется.
Некорректный JSON в теле запроса отклоняется аналогичным образом, до чтения любых полей:
{
"error": "Invalid JSON in request body"
}
Поля proxy и ignoreProxies возвращают собственные ошибки 400. Оба принимают непрозрачные ID прокси из предыдущих ответов, поэтому любые другие значения вызывают ошибку декодирования:
| Сообщение | Что произошло |
|---|---|
Invalid proxy format |
Значение proxy не является ID прокси, выданным FourA. Прямой адрес прокси приводит к этой ошибке. |
Invalid ignoreProxies format |
Один из элементов в ignoreProxies не является ID прокси. |
Proxy not found |
ID успешно декодирован, но больше не указывает на активную точку выхода. Выберите новую. |
Managed exit: this proxy id cannot be pinned to a request |
Точка выхода существует, но FourA не может удерживать ее для явного запроса. ID премиум-узла приводит к этой ошибке, если в вашем тарифе закончился премиум-трафик. Переиспользуйте сессию, в которой он был получен, или выполните вызов через POST /api/proxy/ и используйте автоматически выбранный узел. |
Решение: Убедитесь, что запрос содержит все обязательные поля, URL используют http:// или https://, хост разрешается в публичный адрес, а любое значение proxy представляет собой ID, скопированный без изменений из предыдущего ответа.
Это результаты уровня client_error: запрос не покидал FourA, поэтому с вашего баланса ничего не списано.
401: Unauthorized
API key отсутствует или недействителен.
Отсутствующий ключ:
{
"error": "Missing API key. Include X-API-Key header."
}
Недопустимый ключ:
{
"error": "Invalid API key"
}
Решение: Проверьте, что ваш header X-API-Key содержит действительный ключ. При необходимости сгенерируйте новый ключ в Dashboard.
403: Not in Your Plan
Вызов запросил endpoint или параметр, который не входит в ваш тарифный план. Response устанавливает X-FourA-Limit и возвращает тот же код в body под reason:
{
"error": "The browser endpoint is not included in your plan (Free). Upgrade to use it.",
"reason": "plan_limit_feature",
"documentation": "https://foura.ai/prices"
}
reason возвращает plan_limit_feature для endpoint, исключенного из тарифного плана, или для exitCountries на тарифе без геотаргетинга, а также plan_limit_premium для exitClass: premium на тарифе без премиум-выходов. Строка error указывает имя endpoint или параметра.
Ошибка 403 от FourA никогда не относится к целевому сайту: обращение к нему даже не выполнялось. Если 403 вернул сам целевой сайт, она приходит как HTTP 200 с status: 403 внутри тела ответа.
Решение: удалите параметр, вызовите endpoint, доступный в вашем тарифе, или обновите тарифный план. Заголовок Retry-After не задается, так как ожидание не изменит результат. Средства списаны не были: получен outcome rate_limit, а тарифицируется только success.
413: Payload Too Large
Тело JSON-запроса превышает допустимый для FourA размер (100 КБ). Ответ не является JSON и не содержит X-FourA-Request-Id, так как тело отклоняется до его чтения.
Решение: отправьте payload меньшего размера в data. Средства списаны не были.
429: Rate Limited
Кодом 429 отвечают две разные проверки, и они содержат разные наборы полей.
Лимиты вашего тарифного плана. В ответе устанавливается header X-FourA-Limit с указанием отклонившего запрос лимита, а тот же код передается в теле в поле reason:
{
"error": "Concurrency limit reached: your plan allows 50 simultaneous proxy request(s). Retry when an in-flight request finishes.",
"reason": "plan_limit_concurrency",
"documentation": "https://foura.ai/prices",
"limit": 50,
"in_flight": 51,
"retry_after_seconds": 1
}
reason принимает одно из значений: plan_limit_concurrency, plan_limit_rate, plan_limit_browser_daily, plan_limit_credits или plan_limit_bandwidth. Если ожидание имеет смысл, время ожидания указывается в retry_after_seconds и в заголовке Retry-After, но никогда в retryAfter. plan_limit_browser_daily не содержит ни того, ни другого, так как лимит сбрасывается в полночь по UTC, а не через количество секунд. Списаний не было: результат равен rate_limit, а тарифицируется только success.
Общий лимит платформы. Заголовок X-FourA-Limit отсутствует, время ожидания указано в retryAfter:
{
"error": "Rate limit exceeded",
"status": 429,
"service": "single",
"retryAfter": 5,
"current": { "concurrency": 12, "rpm": 3000 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
current и limits описывают состояние сервиса для всего трафика, а не для вашего аккаунта. Отказ здесь означает, что FourA перегружен.
Решение: подождите время, указанное в Retry-After, retry_after_seconds или retryAfter из ответа. При превышении лимита параллелизма или rate limit ограничьте число открытых запросов вместо повторной отправки отклоненного пакета. При исчерпании дневного лимита или лимита расчетного периода остановите выполнение. См. Rate Limits для описания всех полей и Run Requests in Parallel для шаблона реализации.
500: Server Error
На нашей стороне произошла ошибка.
Решение: повторите запрос через небольшую паузу. Если ошибка повторяется, проверьте страницу статуса или обратитесь в поддержку, указав X-FourA-Request-Id из неудавшегося ответа.
502: Upstream Unavailable
FourA связался со своим движком, но не смог обработать полученный ответ.
{
"error": "Upstream unavailable",
"details": "..."
}
Решение: повторите попытку с небольшой задержкой (backoff). Ошибка возникла на нашей стороне, поэтому она для вас бесплатна: результат service_error, а списывается только success.
504: Upstream Timeout
Движок не уложился в лимит времени для этого request.
{
"error": "Upstream timeout",
"details": "the backend did not finish inside the time budget for this request"
}
Ошибка 504 связана с длительностью выполнения задачи, а не с вашим ключом, параметрами или proxy. Обычные причины: медленные целевые ресурсы, холодное прохождение проверок и тяжелые страницы.
Решение: увеличьте timeout_ms в request (Single принимает до 120000, Browser до 120000, Auto до 180000) или повторите запрос. FourA ожидает указанное вами время плюс небольшой запас, поэтому увеличение лимита действительно дает больше времени.
503: Сервис отключен или перегружен
Код 503 означает, что сервис временно недоступен из-за обслуживания либо исчерпан лимит параллельных запросов платформы. Оба варианта содержат одинаковые ключи: error, status, service, retryAfter, current и limits. Различайте их по строке error, а не по наличию тех или иных полей.
{
"error": "Service disabled",
"status": 503,
"service": "single",
"retryAfter": 60,
"current": { "concurrency": 0, "rpm": 0 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
Service disabled означает техническое обслуживание, и current содержит 0 для обоих счетчиков, так как request был отклонен до проведения измерений. Service at capacity представляет собой вариант с concurrency, и в нем current содержит реальное использование платформы. Описание этого формата см. в разделе Rate Limits.
Решение: подождите retryAfter секунд и повторите попытку. На странице статуса указаны активные окна технического обслуживания.
Третий вариант 503 не содержит retryAfter. Это означает, что движок за вашим endpoint перезапускался в момент поступления вызова:
{
"error": "Backend service unavailable",
"backend_status": 503
}
Повторите попытку через секунду или две.
Чтение ошибок из /api/auto/
POST /api/auto/ отвечает HTTP 200 каждый раз, когда ladder запускался, даже если каждый шаг завершился с ошибкой. Фактический результат находится в теле ответа:
{
"status": 403,
"error": "exit blocked by the target defense",
"attempts": 7,
"meta": { "rung": "fail", "solved": false, "attempts": 7, "credits": 47 }
}
status представляет собой последний статус, возвращенный целевым ресурсом, или 502, если ни одна попытка не увенчалась успехом (504, если сначала истек лимит времени). Поле запроса, которое Auto не может принять (например, timeout_ms меньше 5000 или больше 180000), возвращается аналогично: HTTP 200 с "status": 400 и причиной в error, до выполнения каких-либо попыток и бесплатно.
Поэтому не стройте логику ветвления на транспортном статусе для Auto. Вместо этого считывайте status и error из тела ответа. Настоящий non-200 статус от /api/auto/ означает, что FourA отклонил вызов до запуска цепочки или не смог его завершить: 400 (некорректный JSON, либо приватный или зарезервированный адрес назначения), 401, 413, 502, 503 или 504. Лимиты, ваши или платформы, возвращаются внутри ответа 200 со своим статусом в теле.
Если целевой сайт привел к сбою нескольких вызовов Auto подряд, Auto некоторое время отвечает сразу без повторных попыток: "error": "target temporarily unservable, retry later", "status": 503 и retryAfter в секундах. Это бесплатно; подождите retryAfter секунд.
Превышение лимита тарифа в одном из подзапросов также возвращается как HTTP 200. Тело ответа содержит сам отказ с его reason, а также status и meta, при этом ответ содержит тот же заголовок X-FourA-Limit, что и при прямом отказе:
{
"status": 429,
"error": "Credit limit reached: 75,000 of 75,000 credits used this billing period. Upgrade your plan or wait for the reset.",
"reason": "plan_limit_credits",
"documentation": "https://foura.ai/prices",
"used": 75000,
"hard_stop": 75000,
"retry_after_seconds": 86400,
"resets_at": "2026-10-01T00:00:00.000Z",
"meta": { "rung": "fail", "solved": false, "attempts": 1, "credits": 0 }
}
Какие лимиты останавливают лестницу целиком, а какие закрывают только одну ступень, описано в Smart Fetch (Auto).
Ошибки на стороне цели внутри 200 OK
Не каждая ошибка выражается в виде статуса HTTP, отличного от 2xx. Когда цель отвечает кодом HTTP 200, но ответ FourA содержит error (например, ваши правила validate отклонили тело) или тело представляет собой страницу проверки, которую FourA распознает, итоговый результат равен application_error. Если цель возвращает статус не 2xx, который ваши правила validate не принимают, результатом становится application_fail, и тело ответа передается без изменений.
Ни один из этих случаев не тарифицируется: тарифицируется только success. Браузер также может вернуть HTTP 200 с "error": "No available browser slot", когда все браузеры FourA заняты. Это не тарифицируется; повторите попытку через несколько секунд. Полная таксономия описана в справочнике Outcomes.
Одиночный запрос через зафиксированный вами proxy также может вернуть HTTP 200 с кодом "error": "The exit gave the same answer for <n> different sites" рядом с телом. FourA обнаружил, что эта выходная нода отдает одну и ту же страницу не связанным сайтам, то есть страница принадлежит самой ноде, а не является запрашиваемой. Это статус application_error, и он не тарифицируется. Возьмите новую выходную ноду из POST /api/proxy/, который пропускает такие ноды автоматически.
Кодировка ответа
FourA автоматически декодирует тела ответов в UTF-8. Если целевой сервер отдает windows-1251, gbk, shift_jis, iso-8859-* или любую другую кодировку, указанную в заголовке Content-Type или в теге HTML <meta charset>, вы получаете чистую строку UTF-8 в поле data (single, proxy) или body (browser).
Для бинарных данных (изображения, protobuf, исходный звук) установите параметр returnBuffer: true в запросе. Тогда Single и Proxy возвращают data в виде объекта с необработанными байтами, {"type": "Buffer", "data": [<byte values>]}, без перекодирования набора символов.
Стратегия повторных попыток
Практическая стратегия повторных попыток:
import time
import requests
# Plan limits that no short wait will clear: stop the run instead of retrying.
STOP_ON = {
"plan_limit_feature",
"plan_limit_premium",
"plan_limit_browser_daily",
"plan_limit_credits",
"plan_limit_bandwidth",
}
def make_request(url, payload, api_key, max_retries=3):
for attempt in range(max_retries):
resp = requests.post(
url,
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
json=payload,
)
if resp.status_code == 200:
return resp.json()
body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
request_id = resp.headers.get("X-FourA-Request-Id", "?")
limit = resp.headers.get("X-FourA-Limit")
if limit in STOP_ON:
raise RuntimeError(f"{limit} (request {request_id}): {body.get('error')}")
# Plan limits send Retry-After and retry_after_seconds; platform limits send retryAfter.
header = resp.headers.get("Retry-After")
retry_after = (
int(header) if header and header.isdigit()
else body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt
)
if resp.status_code in (429, 503):
time.sleep(retry_after)
continue
if resp.status_code >= 500: # 500, 502, 503, 504 are all ours to fix
time.sleep(2 ** attempt)
continue
# 400/401/403/404 won't fix themselves
raise RuntimeError(f"{resp.status_code} (request {request_id}): {body.get('error')}")
raise RuntimeError(f"Exhausted {max_retries} retries")
Сбои proxy содержат отчет
Вызов POST /api/proxy/, исчерпавший попытки, возвращается как HTTP 200 с конвертом ошибки, а не как код ошибки HTTP. Строка ошибки короткая и всегда имеет одинаковую структуру, поэтому рядом передается объект attemptReport со счетчиками:
{
"error": "Download maxTry limit reached",
"attemptReport": {
"total": 25,
"noResponse": 0,
"defense": 0,
"contentRejected": 25,
"statusRejected": 0,
"other": 0,
"vendors": [],
"profilesTried": ["default"],
"summary": "25 attempt(s): 25 returned HTTP 200 with no defense present and were rejected only by your validate.data - the page was fetched, your content rule did not match it"
},
"total": 34.812
}
Логируйте attemptReport.summary рядом с ошибкой, и вы узнаете, были ли выходы заблокированы, недоступны или возвращали страницы, отклоненные вашими правилами validate. Справочник полей и действия для каждого счетчика: Почему у proxy request закончились попытки.
Связанные разделы
- Rate Limits: лимиты тарифа, concurrency и сведения о RPM
- Параллельное выполнение requests: соблюдение лимитов concurrency вашего тарифа
- Результаты request: описание семи значений outcome
- Частые проблемы: симптомы, причины, решения
- Проверки сайтов: когда body содержит страницу challenge вместо ошибки
- Почему у proxy request закончились попытки: чтение
attemptReport