From 5d23eb36354d07508a015531f298b3e261bbafce Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 22 Mar 2022 21:16:38 +0100 Subject: wallet: improve error handling and error codes --- packages/taler-wallet-core/src/bank-api-client.ts | 19 +- packages/taler-wallet-core/src/db.ts | 28 +-- packages/taler-wallet-core/src/errors.ts | 216 +++++++++++++-------- .../taler-wallet-core/src/headless/NodeHttpLib.ts | 46 ++--- .../src/operations/backup/index.ts | 10 +- .../taler-wallet-core/src/operations/deposits.ts | 6 +- .../taler-wallet-core/src/operations/exchanges.ts | 22 +-- packages/taler-wallet-core/src/operations/pay.ts | 106 +++++----- .../taler-wallet-core/src/operations/recoup.ts | 6 +- .../taler-wallet-core/src/operations/refresh.ts | 6 +- .../taler-wallet-core/src/operations/refund.ts | 6 +- .../taler-wallet-core/src/operations/reserves.ts | 13 +- packages/taler-wallet-core/src/operations/tip.ts | 16 +- .../taler-wallet-core/src/operations/withdraw.ts | 35 ++-- packages/taler-wallet-core/src/pending-types.ts | 20 +- packages/taler-wallet-core/src/util/http.ts | 92 ++++----- packages/taler-wallet-core/src/wallet.ts | 62 +++--- 17 files changed, 377 insertions(+), 332 deletions(-) (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/bank-api-client.ts b/packages/taler-wallet-core/src/bank-api-client.ts index 128e9a7a7..14bf07174 100644 --- a/packages/taler-wallet-core/src/bank-api-client.ts +++ b/packages/taler-wallet-core/src/bank-api-client.ts @@ -31,7 +31,9 @@ import { getRandomBytes, j2s, Logger, + TalerErrorCode, } from "@gnu-taler/taler-util"; +import { TalerError } from "./errors.js"; import { HttpRequestLibrary, readSuccessResponseJsonOrErrorCode, @@ -104,15 +106,20 @@ export namespace BankApi { let paytoUri = `payto://x-taler-bank/localhost/${username}`; if (resp.status !== 200 && resp.status !== 202) { logger.error(`${j2s(await resp.json())}`); - throw new Error(); - } - const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny()); - // LibEuFin demobank returns payto URI in response - if (respJson.paytoUri) { - paytoUri = respJson.paytoUri; + throw TalerError.fromDetail( + TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, + { + httpStatusCode: resp.status, + }, + ); } try { + // Pybank has no body, thus this might throw. const respJson = await resp.json(); + // LibEuFin demobank returns payto URI in response + if (respJson.paytoUri) { + paytoUri = respJson.paytoUri; + } } catch (e) {} return { password, diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index e9fe6a47b..69606b8ff 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -35,7 +35,7 @@ import { MerchantInfo, Product, RefreshReason, - TalerErrorDetails, + TalerErrorDetail, UnblindedSignature, CoinEnvelope, TalerProtocolTimestamp, @@ -229,7 +229,7 @@ export interface ReserveRecord { * Last error that happened in a reserve operation * (either talking to the bank or the exchange). */ - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; } /** @@ -545,7 +545,7 @@ export interface ExchangeRecord { * Last error (if any) for fetching updated information about the * exchange. */ - lastError?: TalerErrorDetails; + lastError?: TalerErrorDetail; /** * Retry status for fetching updated information about the exchange. @@ -580,7 +580,7 @@ export interface PlanchetRecord { withdrawalDone: boolean; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; /** * Public key of the reserve that this planchet @@ -820,14 +820,14 @@ export interface ProposalRecord { */ retryInfo?: RetryInfo; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; } /** * Status of a tip we got from a merchant. */ export interface TipRecord { - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; /** * Has the user accepted the tip? Only after the tip has been accepted coins @@ -922,9 +922,9 @@ export interface RefreshGroupRecord { */ retryInfo: RetryInfo; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; - lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetails }; + lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetail }; /** * Unique, randomly generated identifier for this group of @@ -1256,7 +1256,7 @@ export interface PurchaseRecord { payRetryInfo?: RetryInfo; - lastPayError: TalerErrorDetails | undefined; + lastPayError: TalerErrorDetail | undefined; /** * Retry information for querying the refund status with the merchant. @@ -1266,7 +1266,7 @@ export interface PurchaseRecord { /** * Last error (or undefined) for querying the refund status with the merchant. */ - lastRefundStatusError: TalerErrorDetails | undefined; + lastRefundStatusError: TalerErrorDetail | undefined; /** * Continue querying the refund status until this deadline has expired. @@ -1400,7 +1400,7 @@ export interface WithdrawalGroupRecord { */ retryInfo: RetryInfo; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; } export interface BankWithdrawUriRecord { @@ -1465,7 +1465,7 @@ export interface RecoupGroupRecord { /** * Last error that occurred, if any. */ - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; } export enum BackupProviderStateTag { @@ -1485,7 +1485,7 @@ export type BackupProviderState = | { tag: BackupProviderStateTag.Retrying; retryInfo: RetryInfo; - lastError?: TalerErrorDetails; + lastError?: TalerErrorDetail; }; export interface BackupProviderTerms { @@ -1598,7 +1598,7 @@ export interface DepositGroupRecord { operationStatus: OperationStatus; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; /** * Retry info. diff --git a/packages/taler-wallet-core/src/errors.ts b/packages/taler-wallet-core/src/errors.ts index 3109644ac..07a01a760 100644 --- a/packages/taler-wallet-core/src/errors.ts +++ b/packages/taler-wallet-core/src/errors.ts @@ -23,63 +23,143 @@ /** * Imports. */ -import { TalerErrorCode, TalerErrorDetails } from "@gnu-taler/taler-util"; +import { + TalerErrorCode, + TalerErrorDetail, + TransactionType, +} from "@gnu-taler/taler-util"; -/** - * This exception is there to let the caller know that an error happened, - * but the error has already been reported by writing it to the database. - */ -export class OperationFailedAndReportedError extends Error { - static fromCode( - ec: TalerErrorCode, - message: string, - details: Record, - ): OperationFailedAndReportedError { - return new OperationFailedAndReportedError( - makeErrorDetails(ec, message, details), - ); - } +export interface DetailsMap { + [TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: { + innerError: TalerErrorDetail; + transactionId?: string; + }; + [TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT]: { + exchangeBaseUrl: string; + }; + [TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE]: { + exchangeProtocolVersion: string; + walletProtocolVersion: string; + }; + [TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK]: {}; + [TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID]: {}; + [TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: { + orderId: string; + claimUrl: string; + }; + [TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: {}; + [TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: { + merchantPub: string; + orderId: string; + }; + [TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH]: { + baseUrlForDownload: string; + baseUrlFromContractTerms: string; + }; + [TalerErrorCode.WALLET_INVALID_TALER_PAY_URI]: { + talerPayUri: string; + }; + [TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR]: {}; + [TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: {}; + [TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {}; + [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {}; + [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {}; + [TalerErrorCode.WALLET_NETWORK_ERROR]: {}; + [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {}; + [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: {}; + [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {}; + [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {}; + [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {}; +} - constructor(public operationError: TalerErrorDetails) { - super(operationError.message); +type ErrBody = Y extends keyof DetailsMap ? DetailsMap[Y] : never; - // Set the prototype explicitly. - Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype); - } +export function makeErrorDetail( + code: C, + detail: ErrBody, + hint?: string, +): TalerErrorDetail { + // FIXME: include default hint? + return { code, hint, ...detail }; } -/** - * This exception is thrown when an error occurred and the caller is - * responsible for recording the failure in the database. - */ -export class OperationFailedError extends Error { - static fromCode( - ec: TalerErrorCode, - message: string, - details: Record, - ): OperationFailedError { - return new OperationFailedError(makeErrorDetails(ec, message, details)); +export function makePendingOperationFailedError( + innerError: TalerErrorDetail, + tag: TransactionType, + uid: string, +): TalerError { + return TalerError.fromDetail(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED, { + innerError, + transactionId: `${tag}:${uid}`, + }); +} + +export class TalerError extends Error { + errorDetail: TalerErrorDetail & T; + private constructor(d: TalerErrorDetail & T) { + super(); + this.errorDetail = d; + Object.setPrototypeOf(this, TalerError.prototype); } - constructor(public operationError: TalerErrorDetails) { - super(operationError.message); + static fromDetail( + code: C, + detail: ErrBody, + hint?: string, + ): TalerError { + // FIXME: include default hint? + return new TalerError({ code, hint, ...detail }); + } - // Set the prototype explicitly. - Object.setPrototypeOf(this, OperationFailedError.prototype); + static fromUncheckedDetail(d: TalerErrorDetail): TalerError { + return new TalerError({ ...d }); + } + + static fromException(e: any): TalerError { + const errDetail = getErrorDetailFromException(e); + return new TalerError(errDetail); + } + + hasErrorCode( + code: C, + ): this is TalerError { + return this.errorDetail.code === code; } } -export function makeErrorDetails( - ec: TalerErrorCode, - message: string, - details: Record, -): TalerErrorDetails { - return { - code: ec, - hint: `Error: ${TalerErrorCode[ec]}`, - details: details, - message, - }; +/** + * Convert an exception (or anything that was thrown) into + * a TalerErrorDetail object. + */ +export function getErrorDetailFromException(e: any): TalerErrorDetail { + if (e instanceof TalerError) { + return e.errorDetail; + } + if (e instanceof Error) { + const err = makeErrorDetail( + TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, + { + stack: e.stack, + }, + `unexpected exception (message: ${e.message})`, + ); + return err; + } + // Something was thrown that is not even an exception! + // Try to stringify it. + let excString: string; + try { + excString = e.toString(); + } catch (e) { + // Something went horribly wrong. + excString = "can't stringify exception"; + } + const err = makeErrorDetail( + TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, + {}, + `unexpected exception (not an exception, ${excString})`, + ); + return err; } /** @@ -89,44 +169,24 @@ export function makeErrorDetails( */ export async function guardOperationException( op: () => Promise, - onOpError: (e: TalerErrorDetails) => Promise, + onOpError: (e: TalerErrorDetail) => Promise, ): Promise { try { return await op(); } catch (e: any) { - if (e instanceof OperationFailedAndReportedError) { + if ( + e instanceof TalerError && + e.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED) + ) { throw e; } - if (e instanceof OperationFailedError) { - await onOpError(e.operationError); - throw new OperationFailedAndReportedError(e.operationError); - } - if (e instanceof Error) { - const opErr = makeErrorDetails( - TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, - `unexpected exception (message: ${e.message})`, - { - stack: e.stack, - }, - ); - await onOpError(opErr); - throw new OperationFailedAndReportedError(opErr); - } - // Something was thrown that is not even an exception! - // Try to stringify it. - let excString: string; - try { - excString = e.toString(); - } catch (e) { - // Something went horribly wrong. - excString = "can't stringify exception"; - } - const opErr = makeErrorDetails( - TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, - `unexpected exception (not an exception, ${excString})`, - {}, - ); + const opErr = getErrorDetailFromException(e); await onOpError(opErr); - throw new OperationFailedAndReportedError(opErr); + throw TalerError.fromDetail( + TalerErrorCode.WALLET_PENDING_OPERATION_FAILED, + { + innerError: e.errorDetail, + }, + ); } } diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts index 2a8c9e36c..df25a1092 100644 --- a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts +++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts @@ -27,7 +27,7 @@ import { } from "../util/http.js"; import { RequestThrottler } from "@gnu-taler/taler-util"; import Axios, { AxiosResponse } from "axios"; -import { OperationFailedError, makeErrorDetails } from "../errors.js"; +import { TalerError } from "../errors.js"; import { Logger, bytesToString } from "@gnu-taler/taler-util"; import { TalerErrorCode, URL } from "@gnu-taler/taler-util"; @@ -55,14 +55,14 @@ export class NodeHttpLib implements HttpRequestLibrary { const parsedUrl = new URL(url); if (this.throttlingEnabled && this.throttle.applyThrottle(url)) { - throw OperationFailedError.fromCode( + throw TalerError.fromDetail( TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED, - `request to origin ${parsedUrl.origin} was throttled`, { requestMethod: method, requestUrl: url, throttleStats: this.throttle.getThrottleStats(url), }, + `request to origin ${parsedUrl.origin} was throttled`, ); } let timeout: number | undefined; @@ -83,13 +83,13 @@ export class NodeHttpLib implements HttpRequestLibrary { maxRedirects: 0, }); } catch (e: any) { - throw OperationFailedError.fromCode( + throw TalerError.fromDetail( TalerErrorCode.WALLET_NETWORK_ERROR, - `${e.message}`, { requestUrl: url, requestMethod: method, }, + `${e.message}`, ); } @@ -105,30 +105,26 @@ export class NodeHttpLib implements HttpRequestLibrary { responseJson = JSON.parse(respText); } catch (e) { logger.trace(`invalid json: '${resp.data}'`); - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "invalid JSON", - { - httpStatusCode: resp.status, - requestUrl: url, - requestMethod: method, - }, - ), + throw TalerError.fromDetail( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + { + httpStatusCode: resp.status, + requestUrl: url, + requestMethod: method, + }, + "Could not parse response body as JSON", ); } if (responseJson === null || typeof responseJson !== "object") { logger.trace(`invalid json (not an object): '${respText}'`); - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "invalid JSON", - { - httpStatusCode: resp.status, - requestUrl: url, - requestMethod: method, - }, - ), + throw TalerError.fromDetail( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + { + httpStatusCode: resp.status, + requestUrl: url, + requestMethod: method, + }, + `invalid JSON`, ); } return responseJson; diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index 48eea56ad..400406ce3 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -48,7 +48,7 @@ import { PreparePayResultType, RecoveryLoadRequest, RecoveryMergeStrategy, - TalerErrorDetails, + TalerErrorDetail, AbsoluteTime, URL, WalletBackupContentV1, @@ -464,7 +464,7 @@ async function incrementBackupRetryInTx( backupProviders: typeof WalletStoresV1.backupProviders; }>, backupProviderBaseUrl: string, - err: TalerErrorDetails | undefined, + err: TalerErrorDetail | undefined, ): Promise { const pr = await tx.backupProviders.get(backupProviderBaseUrl); if (!pr) { @@ -487,7 +487,7 @@ async function incrementBackupRetryInTx( async function incrementBackupRetry( ws: InternalWalletState, backupProviderBaseUrl: string, - err: TalerErrorDetails | undefined, + err: TalerErrorDetail | undefined, ): Promise { await ws.db .mktx((x) => ({ backupProviders: x.backupProviders })) @@ -509,7 +509,7 @@ export async function processBackupForProvider( throw Error("unknown backup provider"); } - const onOpErr = (err: TalerErrorDetails): Promise => + const onOpErr = (err: TalerErrorDetail): Promise => incrementBackupRetry(ws, backupProviderBaseUrl, err); const run = async () => { @@ -700,7 +700,7 @@ export interface ProviderInfo { /** * Last communication issue with the provider. */ - lastError?: TalerErrorDetails; + lastError?: TalerErrorDetail; lastSuccessfulBackupTimestamp?: TalerProtocolTimestamp; lastAttemptedBackupTimestamp?: TalerProtocolTimestamp; paymentProposalIds: string[]; diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 4b976011b..42ce5e7c9 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -36,7 +36,7 @@ import { Logger, NotificationType, parsePaytoUri, - TalerErrorDetails, + TalerErrorDetail, TalerProtocolTimestamp, TrackDepositGroupRequest, TrackDepositGroupResponse, @@ -83,7 +83,7 @@ async function resetDepositGroupRetry( async function incrementDepositRetry( ws: InternalWalletState, depositGroupId: string, - err: TalerErrorDetails | undefined, + err: TalerErrorDetail | undefined, ): Promise { await ws.db .mktx((x) => ({ depositGroups: x.depositGroups })) @@ -111,7 +111,7 @@ export async function processDepositGroup( forceNow = false, ): Promise { await ws.memoProcessDeposit.memo(depositGroupId, async () => { - const onOpErr = (e: TalerErrorDetails): Promise => + const onOpErr = (e: TalerErrorDetail): Promise => incrementDepositRetry(ws, depositGroupId, e); return await guardOperationException( async () => await processDepositGroupImpl(ws, depositGroupId, forceNow), diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index df7eee76d..bbed42288 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -34,7 +34,7 @@ import { Recoup, TalerErrorCode, URL, - TalerErrorDetails, + TalerErrorDetail, AbsoluteTime, hashDenomPub, LibtoolVersion, @@ -64,11 +64,7 @@ import { } from "../util/http.js"; import { DbAccess, GetReadOnlyAccess } from "../util/query.js"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; -import { - guardOperationException, - makeErrorDetails, - OperationFailedError, -} from "../errors.js"; +import { guardOperationException, TalerError } from "../errors.js"; import { InternalWalletState, TrustInfo } from "../common.js"; import { WALLET_CACHE_BREAKER_CLIENT_VERSION, @@ -112,7 +108,7 @@ function denominationRecordFromKeys( async function handleExchangeUpdateError( ws: InternalWalletState, baseUrl: string, - err: TalerErrorDetails, + err: TalerErrorDetail, ): Promise { await ws.db .mktx((x) => ({ exchanges: x.exchanges })) @@ -353,7 +349,7 @@ export async function updateExchangeFromUrl( exchange: ExchangeRecord; exchangeDetails: ExchangeDetailsRecord; }> { - const onOpErr = (e: TalerErrorDetails): Promise => + const onOpErr = (e: TalerErrorDetail): Promise => handleExchangeUpdateError(ws, baseUrl, e); return await guardOperationException( () => updateExchangeFromUrlImpl(ws, baseUrl, acceptedFormat, forceNow), @@ -429,14 +425,13 @@ async function downloadExchangeKeysInfo( logger.info("received /keys response"); if (exchangeKeysJsonUnchecked.denoms.length === 0) { - const opErr = makeErrorDetails( + throw TalerError.fromDetail( TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT, - "exchange doesn't offer any denominations", { exchangeBaseUrl: baseUrl, }, + "exchange doesn't offer any denominations", ); - throw new OperationFailedError(opErr); } const protocolVersion = exchangeKeysJsonUnchecked.version; @@ -446,15 +441,14 @@ async function downloadExchangeKeysInfo( protocolVersion, ); if (versionRes?.compatible != true) { - const opErr = makeErrorDetails( + throw TalerError.fromDetail( TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, - "exchange protocol version not compatible with wallet", { exchangeProtocolVersion: protocolVersion, walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION, }, + "exchange protocol version not compatible with wallet", ); - throw new OperationFailedError(opErr); } const currency = Amounts.parseOrThrow( diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 9521d544f..ce3a26c34 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -25,6 +25,7 @@ * Imports. */ import { + AbsoluteTime, AmountJson, Amounts, codecForContractTerms, @@ -34,7 +35,6 @@ import { ConfirmPayResult, ConfirmPayResultType, ContractTerms, - decodeCrock, Duration, durationMax, durationMin, @@ -43,19 +43,17 @@ import { getRandomBytes, HttpStatusCode, j2s, - kdf, Logger, NotificationType, parsePayUri, PreparePayResult, PreparePayResultType, RefreshReason, - stringToBytes, TalerErrorCode, - TalerErrorDetails, - AbsoluteTime, - URL, + TalerErrorDetail, TalerProtocolTimestamp, + TransactionType, + URL, } from "@gnu-taler/taler-util"; import { EXCHANGE_COINS_LOCK, InternalWalletState } from "../common.js"; import { @@ -74,9 +72,9 @@ import { } from "../db.js"; import { guardOperationException, - makeErrorDetails, - OperationFailedAndReportedError, - OperationFailedError, + makeErrorDetail, + makePendingOperationFailedError, + TalerError, } from "../errors.js"; import { AvailableCoinInfo, @@ -467,7 +465,7 @@ async function recordConfirmPay( async function reportProposalError( ws: InternalWalletState, proposalId: string, - err: TalerErrorDetails, + err: TalerErrorDetail, ): Promise { await ws.db .mktx((x) => ({ proposals: x.proposals })) @@ -550,7 +548,7 @@ async function incrementPurchasePayRetry( async function reportPurchasePayError( ws: InternalWalletState, proposalId: string, - err: TalerErrorDetails, + err: TalerErrorDetail, ): Promise { await ws.db .mktx((x) => ({ purchases: x.purchases })) @@ -575,7 +573,7 @@ export async function processDownloadProposal( proposalId: string, forceNow = false, ): Promise { - const onOpErr = (err: TalerErrorDetails): Promise => + const onOpErr = (err: TalerErrorDetail): Promise => reportProposalError(ws, proposalId, err); await guardOperationException( () => processDownloadProposalImpl(ws, proposalId, forceNow), @@ -602,7 +600,7 @@ async function resetDownloadProposalRetry( async function failProposalPermanently( ws: InternalWalletState, proposalId: string, - err: TalerErrorDetails, + err: TalerErrorDetail, ): Promise { await ws.db .mktx((x) => ({ proposals: x.proposals })) @@ -727,13 +725,13 @@ async function processDownloadProposalImpl( if (r.isError) { switch (r.talerErrorResponse.code) { case TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED: - throw OperationFailedError.fromCode( + throw TalerError.fromDetail( TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED, - "order already claimed (likely by other wallet)", { orderId: proposal.orderId, claimUrl: orderClaimUrl, }, + "order already claimed (likely by other wallet)", ); default: throwUnexpectedRequestError(httpResponse, r.talerErrorResponse); @@ -758,13 +756,17 @@ async function processDownloadProposalImpl( logger.trace( `malformed contract terms: ${j2s(proposalResp.contract_terms)}`, ); - const err = makeErrorDetails( + const err = makeErrorDetail( TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED, - "validation for well-formedness failed", {}, + "validation for well-formedness failed", ); await failProposalPermanently(ws, proposalId, err); - throw new OperationFailedAndReportedError(err); + throw makePendingOperationFailedError( + err, + TransactionType.Payment, + proposalId, + ); } const contractTermsHash = ContractTermsUtil.hashContractTerms( @@ -780,13 +782,17 @@ async function processDownloadProposalImpl( proposalResp.contract_terms, ); } catch (e) { - const err = makeErrorDetails( + const err = makeErrorDetail( TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED, - `schema validation failed: ${e}`, {}, + `schema validation failed: ${e}`, ); await failProposalPermanently(ws, proposalId, err); - throw new OperationFailedAndReportedError(err); + throw makePendingOperationFailedError( + err, + TransactionType.Payment, + proposalId, + ); } const sigValid = await ws.cryptoApi.isValidContractTermsSignature( @@ -796,16 +802,20 @@ async function processDownloadProposalImpl( ); if (!sigValid) { - const err = makeErrorDetails( + const err = makeErrorDetail( TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID, - "merchant's signature on contract terms is invalid", { merchantPub: parsedContractTerms.merchant_pub, orderId: parsedContractTerms.order_id, }, + "merchant's signature on contract terms is invalid", ); await failProposalPermanently(ws, proposalId, err); - throw new OperationFailedAndReportedError(err); + throw makePendingOperationFailedError( + err, + TransactionType.Payment, + proposalId, + ); } const fulfillmentUrl = parsedContractTerms.fulfillment_url; @@ -814,16 +824,20 @@ async function processDownloadProposalImpl( const baseUrlFromContractTerms = parsedContractTerms.merchant_base_url; if (baseUrlForDownload !== baseUrlFromContractTerms) { - const err = makeErrorDetails( + const err = makeErrorDetail( TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH, - "merchant base URL mismatch", { baseUrlForDownload, baseUrlFromContractTerms, }, + "merchant base URL mismatch", ); await failProposalPermanently(ws, proposalId, err); - throw new OperationFailedAndReportedError(err); + throw makePendingOperationFailedError( + err, + TransactionType.Payment, + proposalId, + ); } const contractData = extractContractData( @@ -895,10 +909,8 @@ async function startDownloadProposal( ]); }); - /** - * If we have already claimed this proposal with the same sessionId - * nonce and claim token, reuse it. - */ + /* If we have already claimed this proposal with the same sessionId + * nonce and claim token, reuse it. */ if ( oldProposal && oldProposal.downloadSessionId === sessionId && @@ -1029,7 +1041,7 @@ async function storePayReplaySuccess( async function handleInsufficientFunds( ws: InternalWalletState, proposalId: string, - err: TalerErrorDetails, + err: TalerErrorDetail, ): Promise { logger.trace("handling insufficient funds, trying to re-select coins"); @@ -1319,12 +1331,12 @@ export async function preparePayForUri( const uriResult = parsePayUri(talerPayUri); if (!uriResult) { - throw OperationFailedError.fromCode( + throw TalerError.fromDetail( TalerErrorCode.WALLET_INVALID_TALER_PAY_URI, - `invalid taler://pay URI (${talerPayUri})`, { talerPayUri, }, + `invalid taler://pay URI (${talerPayUri})`, ); } @@ -1503,7 +1515,7 @@ export async function processPurchasePay( proposalId: string, forceNow = false, ): Promise { - const onOpErr = (e: TalerErrorDetails): Promise => + const onOpErr = (e: TalerErrorDetail): Promise => reportPurchasePayError(ws, proposalId, e); return await guardOperationException( () => processPurchasePayImpl(ws, proposalId, forceNow), @@ -1527,9 +1539,8 @@ async function processPurchasePayImpl( lastError: { // FIXME: allocate more specific error code code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, - message: `trying to pay for purchase that is not in the database`, - hint: `proposal ID is ${proposalId}`, - details: {}, + hint: `trying to pay for purchase that is not in the database`, + proposalId: proposalId, }, }; } @@ -1594,10 +1605,10 @@ async function processPurchasePayImpl( resp.status <= 599 ) { logger.trace("treating /pay error as transient"); - const err = makeErrorDetails( + const err = makeErrorDetail( TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - "/pay failed", getHttpResponseErrorDetails(resp), + "/pay failed", ); return { type: ConfirmPayResultType.Pending, @@ -1621,8 +1632,11 @@ async function processPurchasePayImpl( delete purch.payRetryInfo; await tx.purchases.put(purch); }); - // FIXME: Maybe introduce a new return type for this instead of throwing? - throw new OperationFailedAndReportedError(errDetails); + throw makePendingOperationFailedError( + errDetails, + TransactionType.Payment, + proposalId, + ); } if (resp.status === HttpStatusCode.Conflict) { @@ -1692,10 +1706,10 @@ async function processPurchasePayImpl( resp.status >= 500 && resp.status <= 599 ) { - const err = makeErrorDetails( + const err = makeErrorDetail( TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - "/paid failed", getHttpResponseErrorDetails(resp), + "/paid failed", ); return { type: ConfirmPayResultType.Pending, @@ -1703,10 +1717,10 @@ async function processPurchasePayImpl( }; } if (resp.status !== 204) { - throw OperationFailedError.fromCode( + throw TalerError.fromDetail( TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - "/paid failed", getHttpResponseErrorDetails(resp), + "/paid failed", ); } await storePayReplaySuccess(ws, proposalId, sessionId); diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index 84a27966d..56c13f1b0 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -30,7 +30,7 @@ import { j2s, NotificationType, RefreshReason, - TalerErrorDetails, + TalerErrorDetail, TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"; @@ -60,7 +60,7 @@ const logger = new Logger("operations/recoup.ts"); async function incrementRecoupRetry( ws: InternalWalletState, recoupGroupId: string, - err: TalerErrorDetails | undefined, + err: TalerErrorDetail | undefined, ): Promise { await ws.db .mktx((x) => ({ @@ -384,7 +384,7 @@ export async function processRecoupGroup( forceNow = false, ): Promise { await ws.memoProcessRecoup.memo(recoupGroupId, async () => { - const onOpErr = (e: TalerErrorDetails): Promise => + const onOpErr = (e: TalerErrorDetail): Promise => incrementRecoupRetry(ws, recoupGroupId, e); return await guardOperationException( async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow), diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 11f0f6c51..7753992f7 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -43,7 +43,7 @@ import { NotificationType, RefreshGroupId, RefreshReason, - TalerErrorDetails, + TalerErrorDetail, } from "@gnu-taler/taler-util"; import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { amountToPretty } from "@gnu-taler/taler-util"; @@ -714,7 +714,7 @@ async function refreshReveal( async function incrementRefreshRetry( ws: InternalWalletState, refreshGroupId: string, - err: TalerErrorDetails | undefined, + err: TalerErrorDetail | undefined, ): Promise { await ws.db .mktx((x) => ({ @@ -747,7 +747,7 @@ export async function processRefreshGroup( forceNow = false, ): Promise { await ws.memoProcessRefresh.memo(refreshGroupId, async () => { - const onOpErr = (e: TalerErrorDetails): Promise => + const onOpErr = (e: TalerErrorDetail): Promise => incrementRefreshRetry(ws, refreshGroupId, e); return await guardOperationException( async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow), diff --git a/packages/taler-wallet-core/src/operations/refund.ts b/packages/taler-wallet-core/src/operations/refund.ts index 686d545df..d888ff015 100644 --- a/packages/taler-wallet-core/src/operations/refund.ts +++ b/packages/taler-wallet-core/src/operations/refund.ts @@ -40,7 +40,7 @@ import { parseRefundUri, RefreshReason, TalerErrorCode, - TalerErrorDetails, + TalerErrorDetail, URL, codecForMerchantOrderStatusPaid, AbsoluteTime, @@ -88,7 +88,7 @@ async function resetPurchaseQueryRefundRetry( async function incrementPurchaseQueryRefundRetry( ws: InternalWalletState, proposalId: string, - err: TalerErrorDetails | undefined, + err: TalerErrorDetail | undefined, ): Promise { await ws.db .mktx((x) => ({ @@ -592,7 +592,7 @@ export async function processPurchaseQueryRefund( proposalId: string, forceNow = false, ): Promise { - const onOpErr = (e: TalerErrorDetails): Promise => + const onOpErr = (e: TalerErrorDetail): Promise => incrementPurchaseQueryRefundRetry(ws, proposalId, e); await guardOperationException( () => processPurchaseQueryRefundImpl(ws, proposalId, forceNow, true), diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index ac9483631..baa977033 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -34,7 +34,7 @@ import { NotificationType, randomBytes, TalerErrorCode, - TalerErrorDetails, + TalerErrorDetail, AbsoluteTime, URL, } from "@gnu-taler/taler-util"; @@ -47,7 +47,7 @@ import { WalletStoresV1, WithdrawalGroupRecord, } from "../db.js"; -import { guardOperationException, OperationFailedError } from "../errors.js"; +import { guardOperationException, TalerError } from "../errors.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { readSuccessResponseJsonOrErrorCode, @@ -135,7 +135,7 @@ async function incrementReserveRetry( async function reportReserveError( ws: InternalWalletState, reservePub: string, - err: TalerErrorDetails, + err: TalerErrorDetail, ): Promise { await ws.db .mktx((x) => ({ @@ -338,7 +338,7 @@ export async function processReserve( forceNow = false, ): Promise { return ws.memoProcessReserve.memo(reservePub, async () => { - const onOpError = (err: TalerErrorDetails): Promise => + const onOpError = (err: TalerErrorDetail): Promise => reportReserveError(ws, reservePub, err); await guardOperationException( () => processReserveImpl(ws, reservePub, forceNow), @@ -571,7 +571,7 @@ async function updateReserve( if ( resp.status === 404 && result.talerErrorResponse.code === - TalerErrorCode.EXCHANGE_RESERVES_GET_STATUS_UNKNOWN + TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN ) { ws.notify({ type: NotificationType.ReserveNotYetFound, @@ -803,9 +803,8 @@ export async function createTalerWithdrawReserve( return tx.reserves.get(reserve.reservePub); }); if (processedReserve?.reserveStatus === ReserveRecordStatus.BankAborted) { - throw OperationFailedError.fromCode( + throw TalerError.fromDetail( TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK, - "withdrawal aborted by bank", {}, ); } diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 765120294..7b3d36a7c 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -22,7 +22,7 @@ import { parseTipUri, codecForTipPickupGetResponse, Amounts, - TalerErrorDetails, + TalerErrorDetail, NotificationType, TipPlanchetDetail, TalerErrorCode, @@ -44,7 +44,7 @@ import { import { j2s } from "@gnu-taler/taler-util"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; -import { guardOperationException, makeErrorDetails } from "../errors.js"; +import { guardOperationException, makeErrorDetail } from "../errors.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { InternalWalletState } from "../common.js"; import { @@ -163,7 +163,7 @@ export async function prepareTip( async function incrementTipRetry( ws: InternalWalletState, walletTipId: string, - err: TalerErrorDetails | undefined, + err: TalerErrorDetail | undefined, ): Promise { await ws.db .mktx((x) => ({ @@ -192,7 +192,7 @@ export async function processTip( tipId: string, forceNow = false, ): Promise { - const onOpErr = (e: TalerErrorDetails): Promise => + const onOpErr = (e: TalerErrorDetail): Promise => incrementTipRetry(ws, tipId, e); await guardOperationException( () => processTipImpl(ws, tipId, forceNow), @@ -296,10 +296,10 @@ async function processTipImpl( merchantResp.status === 424) ) { logger.trace(`got transient tip error`); - const err = makeErrorDetails( + const err = makeErrorDetail( TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - "tip pickup failed (transient)", getHttpResponseErrorDetails(merchantResp), + "tip pickup failed (transient)", ); await incrementTipRetry(ws, tipRecord.walletTipId, err); // FIXME: Maybe we want to signal to the caller that the transient error happened? @@ -355,10 +355,10 @@ async function processTipImpl( if (!tipRecord) { return; } - tipRecord.lastError = makeErrorDetails( + tipRecord.lastError = makeErrorDetail( TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID, - "invalid signature from the exchange (via merchant tip) after unblinding", {}, + "invalid signature from the exchange (via merchant tip) after unblinding", ); await tx.tips.put(tipRecord); }); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index e4c6f2a6a..1d7bf9303 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -30,7 +30,7 @@ import { NotificationType, parseWithdrawUri, TalerErrorCode, - TalerErrorDetails, + TalerErrorDetail, AbsoluteTime, WithdrawResponse, URL, @@ -42,6 +42,7 @@ import { ExchangeWithdrawRequest, Duration, TalerProtocolTimestamp, + TransactionType, } from "@gnu-taler/taler-util"; import { CoinRecord, @@ -63,9 +64,11 @@ import { } from "../util/http.js"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; import { + getErrorDetailFromException, guardOperationException, - makeErrorDetails, - OperationFailedError, + makeErrorDetail, + makePendingOperationFailedError, + TalerError, } from "../errors.js"; import { InternalWalletState } from "../common.js"; import { @@ -299,15 +302,14 @@ export async function getBankWithdrawalInfo( config.version, ); if (versionRes?.compatible != true) { - const opErr = makeErrorDetails( + throw TalerError.fromDetail( TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE, - "bank integration protocol version not compatible with wallet", { exchangeProtocolVersion: config.version, walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, }, + "bank integration protocol version not compatible with wallet", ); - throw new OperationFailedError(opErr); } const reqUrl = new URL( @@ -526,12 +528,9 @@ async function processPlanchetExchangeRequest( ); return r; } catch (e) { + const errDetail = getErrorDetailFromException(e); logger.trace("withdrawal request failed", e); logger.trace(e); - if (!(e instanceof OperationFailedError)) { - throw e; - } - const errDetails = e.operationError; await ws.db .mktx((x) => ({ planchets: x.planchets })) .runReadWrite(async (tx) => { @@ -542,7 +541,7 @@ async function processPlanchetExchangeRequest( if (!planchet) { return; } - planchet.lastError = errDetails; + planchet.lastError = errDetail; await tx.planchets.put(planchet); }); return; @@ -628,10 +627,10 @@ async function processPlanchetVerifyAndStoreCoin( if (!planchet) { return; } - planchet.lastError = makeErrorDetails( + planchet.lastError = makeErrorDetail( TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID, - "invalid signature from the exchange after unblinding", {}, + "invalid signature from the exchange after unblinding", ); await tx.planchets.put(planchet); }); @@ -797,7 +796,7 @@ export async function updateWithdrawalDenoms( async function incrementWithdrawalRetry( ws: InternalWalletState, withdrawalGroupId: string, - err: TalerErrorDetails | undefined, + err: TalerErrorDetail | undefined, ): Promise { await ws.db .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups })) @@ -821,7 +820,7 @@ export async function processWithdrawGroup( withdrawalGroupId: string, forceNow = false, ): Promise { - const onOpErr = (e: TalerErrorDetails): Promise => + const onOpErr = (e: TalerErrorDetail): Promise => incrementWithdrawalRetry(ws, withdrawalGroupId, e); await guardOperationException( () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow), @@ -919,7 +918,7 @@ async function processWithdrawGroupImpl( let numFinished = 0; let finishedForFirstTime = false; - let errorsPerCoin: Record = {}; + let errorsPerCoin: Record = {}; await ws.db .mktx((x) => ({ @@ -957,12 +956,12 @@ async function processWithdrawGroupImpl( }); if (numFinished != numTotalCoins) { - throw OperationFailedError.fromCode( + throw TalerError.fromDetail( TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE, - `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`, { errorsPerCoin, }, + `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`, ); } diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts index 4b1434bb5..f4e5216bc 100644 --- a/packages/taler-wallet-core/src/pending-types.ts +++ b/packages/taler-wallet-core/src/pending-types.ts @@ -25,7 +25,7 @@ * Imports. */ import { - TalerErrorDetails, + TalerErrorDetail, BalancesResponse, AbsoluteTime, TalerProtocolTimestamp, @@ -71,7 +71,7 @@ export type PendingTaskInfo = PendingTaskInfoCommon & export interface PendingBackupTask { type: PendingTaskType.Backup; backupProviderBaseUrl: string; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; } /** @@ -80,7 +80,7 @@ export interface PendingBackupTask { export interface PendingExchangeUpdateTask { type: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; } /** @@ -124,7 +124,7 @@ export interface PendingReserveTask { */ export interface PendingRefreshTask { type: PendingTaskType.Refresh; - lastError?: TalerErrorDetails; + lastError?: TalerErrorDetail; refreshGroupId: string; finishedPerCoin: boolean[]; retryInfo: RetryInfo; @@ -139,7 +139,7 @@ export interface PendingProposalDownloadTask { proposalTimestamp: TalerProtocolTimestamp; proposalId: string; orderId: string; - lastError?: TalerErrorDetails; + lastError?: TalerErrorDetail; retryInfo?: RetryInfo; } @@ -173,7 +173,7 @@ export interface PendingPayTask { proposalId: string; isReplay: boolean; retryInfo?: RetryInfo; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; } /** @@ -184,14 +184,14 @@ export interface PendingRefundQueryTask { type: PendingTaskType.RefundQuery; proposalId: string; retryInfo: RetryInfo; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; } export interface PendingRecoupTask { type: PendingTaskType.Recoup; recoupGroupId: string; retryInfo: RetryInfo; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; } /** @@ -199,7 +199,7 @@ export interface PendingRecoupTask { */ export interface PendingWithdrawTask { type: PendingTaskType.Withdraw; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; retryInfo: RetryInfo; withdrawalGroupId: string; } @@ -209,7 +209,7 @@ export interface PendingWithdrawTask { */ export interface PendingDepositTask { type: PendingTaskType.Deposit; - lastError: TalerErrorDetails | undefined; + lastError: TalerErrorDetail | undefined; retryInfo: RetryInfo | undefined; depositGroupId: string; } diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts index 79afd5707..31e38b609 100644 --- a/packages/taler-wallet-core/src/util/http.ts +++ b/packages/taler-wallet-core/src/util/http.ts @@ -24,16 +24,16 @@ /** * Imports */ -import { OperationFailedError, makeErrorDetails } from "../errors.js"; import { Logger, Duration, AbsoluteTime, - TalerErrorDetails, + TalerErrorDetail, Codec, j2s, } from "@gnu-taler/taler-util"; import { TalerErrorCode } from "@gnu-taler/taler-util"; +import { makeErrorDetail, TalerError } from "../errors.js"; const logger = new Logger("http.ts"); @@ -125,7 +125,7 @@ type ResponseOrError = export async function readTalerErrorResponse( httpResponse: HttpResponse, -): Promise { +): Promise { const errJson = await httpResponse.json(); const talerErrorCode = errJson.code; if (typeof talerErrorCode !== "number") { @@ -134,16 +134,14 @@ export async function readTalerErrorResponse( errJson, )}`, ); - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Error response did not contain error code", - { - requestUrl: httpResponse.requestUrl, - requestMethod: httpResponse.requestMethod, - httpStatusCode: httpResponse.status, - }, - ), + throw TalerError.fromDetail( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + { + requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, + httpStatusCode: httpResponse.status, + }, + "Error response did not contain error code", ); } return errJson; @@ -151,28 +149,28 @@ export async function readTalerErrorResponse( export async function readUnexpectedResponseDetails( httpResponse: HttpResponse, -): Promise { +): Promise { const errJson = await httpResponse.json(); const talerErrorCode = errJson.code; if (typeof talerErrorCode !== "number") { - return makeErrorDetails( + return makeErrorDetail( TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Error response did not contain error code", { requestUrl: httpResponse.requestUrl, requestMethod: httpResponse.requestMethod, httpStatusCode: httpResponse.status, }, + "Error response did not contain error code", ); } - return makeErrorDetails( + return makeErrorDetail( TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - `Unexpected HTTP status (${httpResponse.status}) in response`, { requestUrl: httpResponse.requestUrl, httpStatusCode: httpResponse.status, errorResponse: errJson, }, + `Unexpected HTTP status (${httpResponse.status}) in response`, ); } @@ -191,14 +189,14 @@ export async function readSuccessResponseJsonOrErrorCode( try { parsedResponse = codec.decode(respJson); } catch (e: any) { - throw OperationFailedError.fromCode( + throw TalerError.fromDetail( TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Response invalid", { requestUrl: httpResponse.requestUrl, httpStatusCode: httpResponse.status, validationError: e.toString(), }, + "Response invalid", ); } return { @@ -220,16 +218,14 @@ export function throwUnexpectedRequestError( httpResponse: HttpResponse, talerErrorResponse: TalerErrorResponse, ): never { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - `Unexpected HTTP status ${httpResponse.status} in response`, - { - requestUrl: httpResponse.requestUrl, - httpStatusCode: httpResponse.status, - errorResponse: talerErrorResponse, - }, - ), + throw TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: httpResponse.requestUrl, + httpStatusCode: httpResponse.status, + errorResponse: talerErrorResponse, + }, + `Unexpected HTTP status ${httpResponse.status} in response`, ); } @@ -251,16 +247,14 @@ export async function readSuccessResponseTextOrErrorCode( const errJson = await httpResponse.json(); const talerErrorCode = errJson.code; if (typeof talerErrorCode !== "number") { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Error response did not contain error code", - { - httpStatusCode: httpResponse.status, - requestUrl: httpResponse.requestUrl, - requestMethod: httpResponse.requestMethod, - }, - ), + throw TalerError.fromDetail( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + { + httpStatusCode: httpResponse.status, + requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, + }, + "Error response did not contain error code", ); } return { @@ -282,16 +276,14 @@ export async function checkSuccessResponseOrThrow( const errJson = await httpResponse.json(); const talerErrorCode = errJson.code; if (typeof talerErrorCode !== "number") { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Error response did not contain error code", - { - httpStatusCode: httpResponse.status, - requestUrl: httpResponse.requestUrl, - requestMethod: httpResponse.requestMethod, - }, - ), + throw TalerError.fromDetail( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + { + httpStatusCode: httpResponse.status, + requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, + }, + "Error response did not contain error code", ); } throwUnexpectedRequestError(httpResponse, errJson); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index bbff465a8..cb8b53adf 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -100,9 +100,8 @@ import { WalletStoresV1, } from "./db.js"; import { - makeErrorDetails, - OperationFailedAndReportedError, - OperationFailedError, + getErrorDetailFromException, + TalerError, } from "./errors.js"; import { exportBackup } from "./operations/backup/export.js"; import { @@ -297,10 +296,10 @@ export async function runPending( try { await processOnePendingOperation(ws, p, forceNow); } catch (e) { - if (e instanceof OperationFailedAndReportedError) { + if (e instanceof TalerError) { console.error( "Operation failed:", - JSON.stringify(e.operationError, undefined, 2), + JSON.stringify(e.errorDetail, undefined, 2), ); } else { console.error(e); @@ -399,10 +398,16 @@ async function runTaskLoop( try { await processOnePendingOperation(ws, p); } catch (e) { - if (e instanceof OperationFailedAndReportedError) { - logger.warn("operation processed resulted in reported error"); - logger.warn(`reported error was: ${j2s(e.operationError)}`); + if ( + e instanceof TalerError && + e.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED) + ) { + logger.warn("operation processed resulted in error"); + logger.warn(`error was: ${j2s(e.errorDetail)}`); } else { + // This is a bug, as we expect pending operations to always + // do their own error handling and only throw WALLET_PENDING_OPERATION_FAILED + // or return something. logger.error("Uncaught exception", e); ws.notify({ type: NotificationType.InternalError, @@ -722,7 +727,7 @@ export async function getClientFromWalletState( const res = await handleCoreApiRequest(ws, op, `${id++}`, payload); switch (res.type) { case "error": - throw new OperationFailedError(res.error); + throw TalerError.fromUncheckedDetail(res.error); case "response": return res.result; } @@ -1040,12 +1045,12 @@ async function dispatchRequestInternal( return []; } } - throw OperationFailedError.fromCode( + throw TalerError.fromDetail( TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, - "unknown operation", { operation, }, + "unknown operation", ); } @@ -1067,34 +1072,13 @@ export async function handleCoreApiRequest( result, }; } catch (e: any) { - if ( - e instanceof OperationFailedError || - e instanceof OperationFailedAndReportedError - ) { - logger.error("Caught operation failed error"); - logger.trace((e as any).stack); - return { - type: "error", - operation, - id, - error: e.operationError, - }; - } else { - try { - logger.error("Caught unexpected exception:"); - logger.error(e.stack); - } catch (e) {} - return { - type: "error", - operation, - id, - error: makeErrorDetails( - TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, - `unexpected exception: ${e}`, - {}, - ), - }; - } + const err = getErrorDetailFromException(e); + return { + type: "error", + operation, + id, + error: err, + }; } } -- cgit v1.2.3