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 отверг батч createTransfers — detail содержит конкатенацию 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 /intents — FORBIDDEN (сервисный ключ неактивен):
POST /intents — LIMIT_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/confirm — VERSION_MISMATCH (формируется inline в confirm-handler.ts):
POST /intents/:id/cancel — UNAUTHORIZED_ACTOR (inline в cancel-handler.ts):
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.