Перейти к содержанию

Ошибки

Любая ошибка приходит единым конвертом с одинаковой формой:

{ "error": { "code": "insufficient_credits", "message": "Insufficient credits" } }
  • code — стабильный машиночитаемый идентификатор. Ветви логику по нему.
  • message — человекочитаемый текст для логов/отладки. Может меняться, не парси его.

HTTP-статус соответствует code (см. таблицу). Тело есть у всех ошибок, включая 5xx.

Ошибки валидации

При невалидном теле/параметрах (422) к конверту добавляется details — пер-полевой разбор из FastAPI:

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": [
      { "loc": ["body", "email"], "msg": "value is not a valid email address", "type": "value_error" }
    ]
  }
}

Без эха значения

details несут только loc / type / msg. Присланное значение не отражается обратно (чтобы пароль или другой секрет не попал в ответ, логи или телеметрию). Указывай пользователю на поле по loc, а не по присланному вводу.

Полный список кодов

HTTP code Когда Что делать клиенту
401 auth_required нет/протух/битый access-токен обновить токен (refresh) и повторить
401 invalid_credentials неверный email/пароль или OAuth-токен показать ошибку входа
402 insufficient_credits на балансе меньше, чем cost эффекта предложить пополнение/бонусы
403 forbidden доступ к чужому ресурсу не повторять; это логическая ошибка клиента
403 registration_required раздел закрыт для гостя: платежи, подписки, маркетплейс, смена пароля, голосовой ввод отправить на экран регистрации; не обновлять токен
404 not_found ресурса нет (эффект, загрузка, генерация) проверить id
409 email_taken регистрация на занятый email предложить вход вместо регистрации
409 already_subscribed подписка на этот план уже есть (в т.ч. неоплаченная) показать существующую (подписки)
409 idempotency_key_reused этот Idempotency-Key уже приходил с другим запросом не повторять; на новое действие нужен новый ключ (повтор запроса)
409 request_in_progress первый запрос с этим Idempotency-Key ещё выполняется (только оплата и подписка) повторить через 1-2 секунды с тем же ключом
413 payload_too_large тело запроса больше лимита не слать большие тела; фото — в S3 по presigned-ссылке (uploads)
413 payload_too_large запись для голосового ввода длиннее или тяжелее предела остановить запись раньше; тот же файл не повторять
415 unsupported_media_type запись для голосового ввода не в поддерживаемом формате или не читается как звук сменить формат записи в приложении; повтор не поможет
422 validation_error тело/параметры не прошли валидацию починить запрос по details
422 characters_required старт генерации плоским списком фото на эффект с несколькими людьми, где из списка не понять, чьё фото чьё прислать фото полем characters (генерации)
429 rate_limited превышен лимит запросов подождать Retry-After секунд и повторить
429 too_many_streams слишком много открытых SSE-стримов закрыть ненужные стримы
501 not_implemented контрактный эндпоинт, ещё не реализованный в текущем milestone не использовать пока
503 provider_unavailable пул ИИ-провайдера временно недоступен повторить позже; это backpressure, не отказ
503 provider_unavailable на голосовом вводе: распознавание речи недоступно или не ответило вовремя предложить ввод текстом; повторить можно
503 provider_unavailable на входе через Google / Apple / VK ID / Яндекс ID: способ не настроен на сервере или провайдер не ответил оставить активными другие способы входа (подробнее)
500 internal_error непредвиденная ошибка сервера повторить позже, залогировать

Деньги не теряются

Если генерация падает или не укладывается в таймаут после списания — кредиты возвращаются автоматически. Статус остаётся failed; факт возврата виден в истории кредитов записью refund. Подробнее — Генерация и realtime.

Повтор запроса: Idempotency-Key

Ответ на POST может не дойти: сеть оборвалась, приложение свернули, истёк таймаут. Сервер при этом запрос уже выполнил, и обычный повтор создаст вторую генерацию со вторым списанием или второй платёж. Заголовок Idempotency-Key говорит серверу, что это тот же запрос:

KEY=$(uuidgen)

curl -X POST "$BASE_URL/v1/generations" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $KEY" \
  -d '{"effect_id":"<EFFECT_ID>","characters":[["<UPLOAD_ID>"]]}'

Выполни команду второй раз с тем же $KEY: придёт тот же id, кредиты спишутся один раз.

Заголовок принимают четыре ручки, все, что тратят кредиты или деньги: POST /v1/generations, POST /v1/studio/generations, POST /v1/payments, POST /v1/subscriptions.

  • Ключ создаёт приложение, один на действие пользователя. Нажатие «Создать» или «Оплатить» получает новый ключ, все автоматические повторы этого нажатия идут с ним же. Удобнее всего UUID. Формат: от 16 до 64 символов из A-Z a-z 0-9 _ -, иначе 422 validation_error.
  • Повтор с тем же ключом и тем же телом возвращает тот же объект с тем же кодом ответа (202 или 201). Состояние в нём текущее: генерация могла успеть перейти в processing, платёж в succeeded.
  • Тот же ключ с другим телом или на соседней ручке - 409 idempotency_key_reused. Это ошибка в приложении, повторять такой запрос бессмысленно. Соседние ручки - это генерация и студия, оплата и подписка: ключ генерации и ключ оплаты друг друга не видят, и совпадение между ними отказом не кончится.
  • Ключ закрепляется только за созданным объектом. Если запрос отклонён (402, 403, 404, 422, 429, 503), ключ остаётся свободным, и его можно прислать снова: после пополнения баланса или когда платёжный центр ответит, запрос выполнится.
  • Ключ виден только своему пользователю и живёт столько же, сколько созданный объект: срока, после которого повтор создал бы дубль, нет.
  • Заголовок необязателен. Без него каждый запрос создаёт новый объект, как и раньше.

409 request_in_progress бывает только у оплаты и подписки: строка платежа появляется до ответа платёжного центра, и повтор может прийти в этот промежуток. Повтори через секунду-две с тем же ключом. Дольше полутора минут этот ответ не держится: если первый запрос оборвался на стороне сервера, повтор с тем же ключом откроет оплату заново. Второго списания не будет, потому что страницу оплаты по первому заказу никто не получил.

Лимиты запросов (429)

Чувствительные эндпоинты защищены rate-лимитами (token-bucket). Превышение → 429 rate_limited. Это не отказ, а просьба сбавить темп: в ответе есть заголовок Retry-After (секунды до момента, когда запрос точно пройдёт) — используй его вместо угадывания паузы, дальше повторяй с backoff и джиттером.

Retry-After приходит и на «доменных» 429, где срок известен: кулдаун после отказа и блокировка за серию отказов в заявках на свои эффекты. Исключения — те доменные лимиты, срок снятия которых мы не контролируем и выдумывать не станем: «слишком много заявок на модерации» (снимается, когда модератор разберёт очередь) и «слишком много незакрытых платежей» (снимается, когда платёж станет терминальным, максимум через 30 минут — см. Оплата). Правило для клиента: есть заголовок — ждём столько; нет — обычный backoff.

  • Auth (/auth/register, /login, /refresh, /change-password, /google, /apple, /vk, /yandex) — лимит на источник запросов: IPv4-адрес или подсеть /64 для IPv6. Плюс на /login — блокировка после серии неверных паролей подряд: считается по паре «email и источник» и действует ограниченное время. Вход в тот же аккаунт из другой сети она не задевает.
  • Запись (POST /generations, POST /uploads, POST /user-effects, DELETE /user-effects/{id}, POST /effects/{id}/complaints, POST /devices, POST /payments) — лимит по пользователю: всплеск запросов с одним токеном упрётся в 429.
  • Голосовой ввод (POST /speech/transcriptions): свой лимит по пользователю, заметно уже записи: запрос уходит по нажатию кнопки микрофона, а не по мере записи.
  • Чтения под токеном (/me, /credits/*, /generations, /user-effects/*, /referral, /payments/*) — один общий бюджет на пользователя, а не на эндпоинт: чередование URL его не обходит. Размер рассчитан на человека с телефоном (пачка экранов открывается разом, обычное листание не упирается), но опрос в цикле упрётся. Ждать статус генерации, заявки или платежа нужно стримом, а не поллингом (см. ниже).
  • SSE-стримы (GET /generations/{id}/stream, GET /user-effects/{id}/stream, GET /payments/{id}/stream) — под общим бюджетом чтений на открытие, но главное ограничение другое: лимит одновременно открытых стримов на пользователя. Сверх него — 429 с кодом too_many_streams (а не rate_limited): ждать бесполезно, нужно закрыть стримы, которые больше не смотришь. На терминальном статусе стрим закрывается сам.

На стенде/проде поверх приложения работает ещё и edge-лимит (по реальному IP клиента). Конкретные пороги — конфигурируемые; рассчитывай на «нормальный» темп UI, а не на циклы.

Рекомендации

  • Один общий обработчик ответа: распарсил конверт → переключился по code.
  • На 401 auth_required — единая логика обновления токена (не дублировать на каждом вызове).
  • 403 registration_required в эту логику попадать не должен: токен гостя валиден, обновление вернёт такую же пару, и клиент зациклится. Это ответ «зарегистрируйся», а не «обновись».
  • На 429/503/5xx — повтор с экспоненциальной задержкой и джиттером, конечное число попыток.
  • POST, который тратит кредиты или деньги, повторяй только с Idempotency-Key: после обрыва связи или 5xx неизвестно, выполнился ли запрос, и повтор без ключа спишет второй раз.
  • Никогда не показывай пользователю сырой message как UI-текст — маппь code в свои строки. На 5xx message намеренно обобщённый (Internal server error) — диагностика по логам, не по тексту.
  • Каждый ответ несёт заголовок X-Request-Id. Логируй его на клиенте и прикладывай к багрепортам — по нему запрос мгновенно находится в серверных логах.