Ошибки¶
Любая ошибка приходит единым конвертом с одинаковой формой:
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в свои строки. На5xxmessageнамеренно обобщённый (Internal server error) — диагностика по логам, не по тексту. - Каждый ответ несёт заголовок
X-Request-Id. Логируй его на клиенте и прикладывай к багрепортам — по нему запрос мгновенно находится в серверных логах.