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

Обработка ошибок

Платформа использует стандартные HTTP-коды и единый JSON-формат ошибок во всех эндпоинтах.


HTTP-коды

Код Значение Когда возникает
200 OK Успех Успешные GET/PUT/PATCH-запросы
201 Created Ресурс создан Успешный POST, создающий новую сущность
204 No Content Успех без тела Успешные DELETE или назначение роли
400 Bad Request Ошибка валидации или бизнес-правила Некорректный ввод, отсутствие обязательных полей, нарушение доменных правил
401 Unauthorized Аутентификация не прошла Отсутствует, недействителен или просрочен ключ; неправильная подпись
403 Forbidden Нет прав Доступ к ресурсу другой организации, отсутствие нужного скоупа, IP вне списка разрешённых
404 Not Found Ресурс не существует ID не существует или принадлежит другой организации
409 Conflict Конфликт уникальности Например, кошелёк по такому активу уже выпущен
429 Too Many Requests Превышен лимит запросов Исчерпан бюджет запросов на API-ключ или IP. См. Надёжность и лимиты → Лимиты запросов.
500 Internal Server Error Серверная ошибка Необработанное исключение (логируется на сервере)
502 Bad Gateway Сбой провайдера хранения Кастоди-бэкенд вернул ошибку или недоступен

Формат ошибки

Все ответы об ошибке используют единую структуру:

{
  "statusCode": 400,
  "message": "Человекочитаемое описание ошибки.",
  "errors": {
    "generalErrors": [
      "Человекочитаемое описание ошибки."
    ]
  }
}
Поле Тип Описание
statusCode integer HTTP-код
message string Краткое описание
errors.generalErrors string[] Один или несколько текстов ошибок

Для ошибок валидации (от FastEndpoints) в ответе также могут быть ошибки по конкретным полям:

{
  "statusCode": 400,
  "message": "Произошли одна или несколько ошибок.",
  "errors": {
    "email": ["Требуется корректный email."],
    "password": ["Пароль должен быть не короче 8 символов."]
  }
}

Ответы уровня 500

Из соображений безопасности ошибки уровня 500 не раскрывают внутренние детали — тело ответа всегда содержит обобщённое сообщение:

{
  "statusCode": 500,
  "message": "One or more errors occurred.",
  "errors": {
    "generalErrors": ["An unexpected error occurred."]
  }
}

Стратегии обработки

Транзиентные ошибки — повторяйте

502 Bad Gateway от кастоди-бэкенда — почти всегда временная. Повторите с экспоненциальной задержкой (например, 1 с, 2 с, 4 с, 8 с, 16 с) до максимум 5 попыток. Если после этого 502 сохраняется — поднимайте алерт.

Ошибки авторизации — не повторяйте

401 обычно означает:

  • Системные часы клиента ушли больше чем на 30 секунд от серверных → синхронизируйте через NTP.
  • Подпись посчитана неверно — пересмотрите алгоритм. См. Аутентификацию.
  • Ключ отозван — попросите администратора создать новый.
  • IP запроса вне списка разрешённых.

403 означает «у ключа нет нужного скоупа». Запросите у администратора ключ с подходящим набором скоупов.

Ошибки валидации — исправляйте ввод

400 всегда возвращает машиночитаемые errors в ответе — используйте их для обратной связи пользователю или для починки запроса.

Конфликты — обрабатывайте идемпотентно

409 обычно значит, что вы попытались создать что-то уже существующее (дубликат email-а, кошелька по тому же активу и т. д.). Идемпотентный код обращается за существующим ресурсом вместо повторного создания.

Ошибки доменных правил

Некоторые ответы 400 кодируют нарушения бизнес-логики, а не ошибки валидации, например:

{
  "statusCode": 400,
  "message": "No outbound webhook configured."
}

Другие примеры:

  • "Source and destination wallets must be for the same asset."
  • "Insufficient balance."

Такие ошибки не исчезнут после повтора — поправьте запрос или исходное состояние и отправьте заново.


Пример обработки ошибок (Node.js)

async function callApi(method, path, body) {
  const headers = signRequest(method, path, body);  // см. Аутентификацию
  headers["Content-Type"] = "application/json";

  const response = await fetch(`${BASE_URL}${path}`, {
    method,
    headers,
    body: body || undefined,
  });

  if (response.ok) return response.json();

  const error = await response.json();
  switch (response.status) {
    case 400:  // некорректный ввод — покажите error.errors пользователю
      throw new ValidationError(error);
    case 401:  // плохой ключ / подпись / расхождение часов — НЕ повторять
    case 403:  // нет нужного скоупа / IP не разрешён — НЕ повторять
      throw new AuthError(error);
    case 404:
      throw new NotFoundError(error);
    case 409:
      throw new ConflictError(error);
    case 502:  // транзиентный сбой кастоди-бэкенда — повтор с задержкой
      await sleep(2000);
      return callApi(method, path, body);
    default:
      throw new Error(error.message);
  }
}