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

Errors — таксономия и формат ответов

Единая схема ошибок Payment Manager: все ошибки сериализуются через setErrorHandler в src/server.ts и наследуются от PaymentError (см. src/shared/errors.ts). Каждый код ошибки — стабильный машинно-читаемый идентификатор, по которому клиент принимает решение о ретрае/UX.

Полный реестр кодов с описанием каждого — в ../reference/passport/05-error-codes.md.

Формат ответа

Реальный обработчик из src/server.ts (app.setErrorHandler(...)):

// PaymentError → status = error.statusCode
{
  "error":   "LIMIT_EXCEEDED",          // машинный код (string)
  "message": "Payment limit exceeded",  // человекочитаемое сообщение
  "detail":  {                          // опционально, BigInt сериализуется как строка
    "ruleName": "daily_p2p",
    "window":   "1d",
    "limitType":"amount",
    "limit":    "100000",
    "current":  "95000",
    "requested":"10000"
  }
}

Особенности обработчика:

  • Если брошена PaymentError (или подкласс) — reply.status(error.statusCode).send({ error, message, detail? }). Поле detail добавляется только если у инстанса есть свойство detail (используется LimitExceededError). Все bigint в detail сериализуются через JSON.stringify(..., bigint → string).
  • Если в ошибке есть массив issues (Zod) — status 422, тело { error: "VALIDATION_ERROR", message: "<path>: <msg>; ..." }.
  • Если у ошибки выставлен validation (Fastify-валидация) — status 422, { error: "VALIDATION_ERROR", message }.
  • Любая прочая необработанная ошибка — status 500, { error: "INTERNAL_ERROR", message }; message = error.message только в NODE_ENV !== 'production', иначе "Internal server error".
  • Ошибки c statusCode >= 500 логируются как error, остальные — как warn (с полями code, status, url, method).

Note: некоторые роуты (/intents/:id/confirm, /intents/:id/cancel, /admin/intents/:id/resolve) формируют ответы вручную через reply.status(...).send({ error: '<CODE>', ... }) — формат совпадает (поле error — машинный код), но без поля message.

Таксономия HTTP-семейств

400 — Bad Request

Неверный синтаксис запроса, отсутствуют/запрещены поля. Бросаются BadRequestError (или NoRouteError).

Код Класс Где Семантика
BAD_REQUEST BadRequestError (default) src/intent/handler.ts, src/operation-types/* Отсутствует/некорректный header (X-User-Id), запрещённое поле (recipientUserId, externalRef, toTbAccountId для операций, где они не разрешены), нерезолвящиеся account-имена.
NO_ROUTE NoRouteError (src/intent/errors.ts) src/intent/router.ts Нет строки в payment_route для пары (operationType, amount).
PAYER_ACCOUNT_USER_MISMATCH BadRequestError (custom code) src/intent/confirm-handler.ts payerAccount.userId ≠ body.payerUserId при подтверждении инвойса.

401 — Unauthorized

HMAC/admin-токен не прошёл проверку. Бросается UnauthorizedError.

Код Класс Где Семантика
UNAUTHORIZED UnauthorizedError src/auth/hmacPlugin.ts, src/auth/adminPlugin.ts Нет заголовков HMAC, неизвестный X-Service-Id, неверная подпись, истёкший X-Timestamp (окно ±60 с), отсутствует/некорректный Authorization: Bearer.

403 — Forbidden

Аутентификация прошла, но действие запрещено.

Код Класс Где Семантика
FORBIDDEN ForbiddenError src/intent/handler.ts, src/intent/from-account-override.ts, src/admin/resolve-intent.ts Сервисный ключ неактивен, operationType не в allowedOperationTypes, fromAccountName/toAccountName override не разрешён, toTbAccountId не разрешён, нет forceResolve permission.
UNAUTHORIZED_ACTOR inline reply.status(403) src/intent/cancel-handler.ts Пользователь пытается отменить чужой инвойс (actorUserId ≠ issuedByUserId).

404 — Not Found

Ресурс не существует или не виден текущему serviceId.

Код Класс Где Семантика
NOT_FOUND NotFoundError (default) src/intent/handler.ts, src/admin/resolve-intent.ts Дженерик — например, intent не найден для текущего сервиса (GET /intents/:id, GET /intents/:id/events).
INTENT_NOT_FOUND NotFoundError (custom message) src/intent/confirm-handler.ts, src/intent/cancel-handler.ts Конкретно — инвойс/intent не найден при confirm/cancel.
PAYER_ACCOUNT_NOT_FOUND NotFoundError (custom message) src/intent/confirm-handler.ts payerAccountName не резолвится в tb_account_map.

408 — Request Timeout

Код Класс Где Семантика
PENDING_EXPIRED PendingExpiredError src/intent/errors.ts TigerBeetle pending-transfer истёк до того, как успели postPending/voidPending.

409 — Conflict

Состояние ресурса не совместимо с операцией. ConflictError + inline-ответы.

Код Класс Где Семантика
CANNOT_CANCEL ConflictError src/channels/merchant-invoice.ts Попытка отменить инвойс, который уже не в CREATED.
ALREADY_PROCESSED inline reply.status(409) src/intent/confirm-handler.ts Повторный confirm уже settled/cancelled инвойса (идемпотентный replay).
EXPIRED inline reply.status(409) src/intent/confirm-handler.ts Confirm после expiresAt — статус уже EXPIRED.
VERSION_MISMATCH inline reply.status(409) src/intent/confirm-handler.ts Optimistic concurrency — body.version ≠ intent.version.
WRONG_STATUS inline reply.status(409) src/admin/resolve-intent.ts Admin force-resolve по intent в нелегальном для операции статусе.
TRANSACTION_IN_PROGRESS TransactionInProgressError src/shared/errors.ts На пользователе уже есть активная транзакция (защита от гонок на client side).

422 — Unprocessable Entity

Запрос корректен по форме, но бизнес-валидация провалилась.

Код Класс Где Семантика
VALIDATION_ERROR ValidationError / Zod / Fastify-validation src/ledger/routes.ts, server-handler Пустой/некорректный X-User-Id, mismatch body.userId, любая Zod-ошибка тела.
ACCOUNT_NOT_FOUND AccountNotFoundError (src/intent/errors.ts) src/intent/handler.ts, src/intent/context-builder.ts Не найден from/to/transit-аккаунт по имени или toTbAccountId.
IPPS_NOT_REGISTERED IppsNotRegisteredError src/intent/errors.ts У пользователя нет IPPS-кошелька — нужно сначала POST /accounts/register-ipps.
IPPS_METADATA_INVALID IppsMetadataInvalidError src/intent/errors.ts metadata для IPPS не прошла валидацию (отсутствует/невалидный PromptPay ID и т.д.).
INSUFFICIENT_FUNDS InsufficientFundsError src/shared/errors.ts Не хватает средств — определяется TB перед createTransfers.
LIMIT_EXCEEDED LimitExceededError src/shared/errors.ts, src/limits/check-limits.ts Превышен лимит (amount/count, daily/monthly/...). Тело содержит detail: LimitExceededDetail.
EVAL_ERROR inline reply.status(422) src/admin/fee-rules.ts Fee-rule expression бросил исключение или вернул объект неправильной формы.

500 / 503 — Server Errors

Код Класс Где Семантика
TB_TRANSFER_ERROR TBTransferError src/intent/errors.ts TigerBeetle отверг батч createTransfersdetail содержит конкатенацию result/status из ответа TB.
INTERNAL_ERROR catch-all src/server.ts Любая необработанная ошибка. message маскируется в production.

Маппинг error class → HTTP status

Сводная таблица для имплементации клиентов (из src/intent/errors.ts и src/shared/errors.ts):

Класс Код по умолчанию HTTP
BadRequestError BAD_REQUEST (override) 400
NoRouteError NO_ROUTE 400
UnauthorizedError UNAUTHORIZED 401
ForbiddenError FORBIDDEN 403
NotFoundError NOT_FOUND 404
PendingExpiredError PENDING_EXPIRED 408
ConflictError CONFLICT (override) 409
TransactionInProgressError TRANSACTION_IN_PROGRESS 409
ValidationError VALIDATION_ERROR (override) 422
AccountNotFoundError ACCOUNT_NOT_FOUND 422
IppsNotRegisteredError IPPS_NOT_REGISTERED 422
IppsMetadataInvalidError IPPS_METADATA_INVALID 422
InsufficientFundsError INSUFFICIENT_FUNDS 422
LimitExceededError LIMIT_EXCEEDED 422
TBTransferError TB_TRANSFER_ERROR 500
(необработанная) INTERNAL_ERROR 500

Семейства ConflictError/BadRequestError/ValidationError принимают code вторым параметром — отсюда коды CANNOT_CANCEL, PAYER_ACCOUNT_USER_MISMATCH и пр. (тот же класс, специализированный код).

Примеры реальных ответов

POST /intentsFORBIDDEN (сервисный ключ неактивен):

// HTTP 403
{
  "error":   "FORBIDDEN",
  "message": "Service auth-center is inactive"
}

POST /intentsLIMIT_EXCEEDED с детализированным detail:

// HTTP 422
{
  "error":   "LIMIT_EXCEEDED",
  "message": "Payment limit exceeded",
  "detail": {
    "ruleName":  "p2p_daily",
    "window":    "1d",
    "limitType": "amount",
    "limit":     "5000000",
    "current":   "4900000",
    "requested": "200000"
  }
}

POST /intents/:id/confirmVERSION_MISMATCH (формируется inline в confirm-handler.ts):

// HTTP 409
{
  "error": "VERSION_MISMATCH",
  "currentVersion": 2
}

POST /intents/:id/cancelUNAUTHORIZED_ACTOR (inline в cancel-handler.ts):

// HTTP 403
{ "error": "UNAUTHORIZED_ACTOR" }

POST /intents — Zod-валидация тела (handled через 'issues' in error):

// HTTP 422
{
  "error":   "VALIDATION_ERROR",
  "message": "amount: Expected string, received number; currency: Required"
}

Связанные документы

  • reference/passport/05-error-codes.md — канонический реестр всех кодов с детальным описанием каждого.
  • auth.md — HMAC-аутентификация (источник 401-ошибок).
  • intents.md — конкретные коды по эндпоинту POST /intents.