diff options
author | Florian Dold <florian@dold.me> | 2022-09-19 12:13:31 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-09-19 12:13:31 +0200 |
commit | fd752f3171a76129d2f615535b90c6bebb88d842 (patch) | |
tree | 93b49a6e5636cd7eeac2655ec0905aeb0b5b241c | |
parent | 548cecca212bb40d56a868736d998c484d721f65 (diff) |
wallet-core: hide transient pay errors
-rw-r--r-- | packages/taler-wallet-core/src/errors.ts | 23 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay.ts | 62 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/retries.ts | 3 |
3 files changed, 76 insertions, 12 deletions
diff --git a/packages/taler-wallet-core/src/errors.ts b/packages/taler-wallet-core/src/errors.ts index d56e936c0..62bde667d 100644 --- a/packages/taler-wallet-core/src/errors.ts +++ b/packages/taler-wallet-core/src/errors.ts @@ -70,6 +70,9 @@ export interface DetailsMap { [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {}; [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {}; [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {}; + [TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR]: { + requestError: TalerErrorDetail; + }; } type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never; @@ -79,7 +82,9 @@ export function makeErrorDetail<C extends TalerErrorCode>( detail: ErrBody<C>, hint?: string, ): TalerErrorDetail { - // FIXME: include default hint? + if (!hint && !(detail as any).hint) { + hint = getDefaultHint(code); + } return { code, hint, ...detail }; } @@ -99,6 +104,15 @@ export function summarizeTalerErrorDetail(ed: TalerErrorDetail): string { return `Error (${ed.code}/${errName})`; } +function getDefaultHint(code: number): string { + const errName = TalerErrorCode[code]; + if (errName) { + return `Error (${errName})`; + } else { + return `Error (<unknown>)`; + } +} + export class TalerError<T = any> extends Error { errorDetail: TalerErrorDetail & T; private constructor(d: TalerErrorDetail & T) { @@ -113,12 +127,7 @@ export class TalerError<T = any> extends Error { hint?: string, ): TalerError { if (!hint) { - const errName = TalerErrorCode[code]; - if (errName) { - hint = `Error (${errName})`; - } else { - hint = `Error (<unknown>)`; - } + hint = getDefaultHint(code); } return new TalerError<unknown>({ code, hint, ...detail }); } diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index a498ab28d..fb22d0fad 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -83,6 +83,7 @@ import { EXCHANGE_COINS_LOCK, InternalWalletState, } from "../internal-wallet-state.js"; +import { PendingTaskType } from "../pending-types.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { CoinSelectionTally, @@ -105,7 +106,11 @@ import { RetryTags, scheduleRetry, } from "../util/retries.js"; -import { spendCoins } from "../wallet.js"; +import { + spendCoins, + storeOperationError, + storeOperationPending, +} from "../wallet.js"; import { getExchangeDetails } from "./exchanges.js"; import { getTotalRefreshCost } from "./refresh.js"; import { makeEventId } from "./transactions.js"; @@ -1519,10 +1524,43 @@ export async function runPayForConfirmPay( transactionId: makeEventId(TransactionType.Payment, proposalId), }; } - case OperationAttemptResultType.Error: - // FIXME: allocate error code! - throw Error("payment failed"); + case OperationAttemptResultType.Error: { + // We hide transient errors from the caller. + const opRetry = await ws.db + .mktx((x) => [x.operationRetries]) + .runReadOnly(async (tx) => + tx.operationRetries.get(RetryTags.byPaymentProposalId(proposalId)), + ); + const maxRetry = 3; + const numRetry = opRetry?.retryInfo.retryCounter ?? 0; + if ( + res.errorDetail.code === + TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR && + numRetry < maxRetry + ) { + // Pretend the operation is pending instead of reporting + // an error, but only up to maxRetry attempts. + await storeOperationPending( + ws, + RetryTags.byPaymentProposalId(proposalId), + ); + return { + type: ConfirmPayResultType.Pending, + lastError: opRetry?.lastError, + transactionId: makeEventId(TransactionType.Payment, proposalId), + }; + } else { + // FIXME: allocate error code! + await storeOperationError( + ws, + RetryTags.byPaymentProposalId(proposalId), + res.errorDetail, + ); + throw Error("payment failed"); + } + } case OperationAttemptResultType.Pending: + await storeOperationPending(ws, `${PendingTaskType.Pay}:${proposalId}`); return { type: ConfirmPayResultType.Pending, transactionId: makeEventId(TransactionType.Payment, proposalId), @@ -1536,7 +1574,7 @@ export async function runPayForConfirmPay( } /** - * Add a contract to the wallet and sign coins, and send them. + * Confirm payment for a proposal previously claimed by the wallet. */ export async function confirmPay( ws: InternalWalletState, @@ -1698,6 +1736,20 @@ export async function processPurchasePay( ); logger.trace(`got resp ${JSON.stringify(resp)}`); + + if (resp.status >= 500 && resp.status <= 599) { + const errDetails = await readUnexpectedResponseDetails(resp); + return { + type: OperationAttemptResultType.Error, + errorDetail: makeErrorDetail( + TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR, + { + requestError: errDetails, + }, + ), + }; + } + if (resp.status === HttpStatusCode.BadRequest) { const errDetails = await readUnexpectedResponseDetails(resp); logger.warn("unexpected 400 response for /pay"); diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts index b13e9a27b..cef9e072c 100644 --- a/packages/taler-wallet-core/src/util/retries.ts +++ b/packages/taler-wallet-core/src/util/retries.ts @@ -205,6 +205,9 @@ export namespace RetryTags { export function forBackup(backupRecord: BackupProviderRecord): string { return `${PendingTaskType.Backup}:${backupRecord.baseUrl}`; } + export function byPaymentProposalId(proposalId: string): string { + return `${PendingTaskType.Pay}:${proposalId}`; + } } export async function scheduleRetryInTx( |