diff options
author | Florian Dold <florian@dold.me> | 2022-03-22 21:16:38 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-03-22 21:16:38 +0100 |
commit | 5d23eb36354d07508a015531f298b3e261bbafce (patch) | |
tree | fae0d2599c94d88c9264bb63a301adb1706824c1 /packages/taler-wallet-core/src/errors.ts | |
parent | f8d12f7b0d4af1b1769b89e80c87f9c169678564 (diff) | |
download | wallet-core-5d23eb36354d07508a015531f298b3e261bbafce.tar.xz |
wallet: improve error handling and error codes
Diffstat (limited to 'packages/taler-wallet-core/src/errors.ts')
-rw-r--r-- | packages/taler-wallet-core/src/errors.ts | 216 |
1 files changed, 138 insertions, 78 deletions
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<string, unknown>, - ): 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> = Y extends keyof DetailsMap ? DetailsMap[Y] : never; - // Set the prototype explicitly. - Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype); - } +export function makeErrorDetail<C extends TalerErrorCode>( + code: C, + detail: ErrBody<C>, + 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<string, unknown>, - ): 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<T = any> 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<C extends TalerErrorCode>( + code: C, + detail: ErrBody<C>, + hint?: string, + ): TalerError { + // FIXME: include default hint? + return new TalerError<unknown>({ code, hint, ...detail }); + } - // Set the prototype explicitly. - Object.setPrototypeOf(this, OperationFailedError.prototype); + static fromUncheckedDetail(d: TalerErrorDetail): TalerError { + return new TalerError<unknown>({ ...d }); + } + + static fromException(e: any): TalerError { + const errDetail = getErrorDetailFromException(e); + return new TalerError(errDetail); + } + + hasErrorCode<C extends keyof DetailsMap>( + code: C, + ): this is TalerError<DetailsMap[C]> { + return this.errorDetail.code === code; } } -export function makeErrorDetails( - ec: TalerErrorCode, - message: string, - details: Record<string, unknown>, -): 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<T>( op: () => Promise<T>, - onOpError: (e: TalerErrorDetails) => Promise<void>, + onOpError: (e: TalerErrorDetail) => Promise<void>, ): Promise<T> { 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, + }, + ); } } |