diff options
author | Sebastian <sebasjm@gmail.com> | 2023-10-17 11:17:18 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-10-17 11:17:41 -0300 |
commit | 503cbfbb95828677b83212816951eb501de2a8fe (patch) | |
tree | 9bbb59300069fba59baca66f543c5145911905b5 /packages/taler-util | |
parent | aca3bc9423f15354913d0114cafbd4bd1782d801 (diff) | |
download | wallet-core-503cbfbb95828677b83212816951eb501de2a8fe.tar.gz wallet-core-503cbfbb95828677b83212816951eb501de2a8fe.tar.bz2 wallet-core-503cbfbb95828677b83212816951eb501de2a8fe.zip |
bank api now return typed errors for documented errors
Diffstat (limited to 'packages/taler-util')
-rw-r--r-- | packages/taler-util/src/errors.ts | 13 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/bank-core.ts | 254 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/index.ts | 0 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/utils.ts | 65 | ||||
-rw-r--r-- | packages/taler-util/src/http-common.ts | 31 |
5 files changed, 309 insertions, 54 deletions
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts index 07a402413..dcdf56c39 100644 --- a/packages/taler-util/src/errors.ts +++ b/packages/taler-util/src/errors.ts @@ -34,6 +34,19 @@ import { type empty = Record<string, never>; +export interface HttpErrors { + // timeout + [TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT]: empty; + // throttled + [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: empty; + // parsing + [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: empty; + // network + [TalerErrorCode.WALLET_NETWORK_ERROR]: empty; + // everything else + [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: empty; +} + export interface DetailsMap { [TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: { innerError: TalerErrorDetail; diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts index c77f9ddda..7b4bb53d4 100644 --- a/packages/taler-util/src/http-client/bank-core.ts +++ b/packages/taler-util/src/http-client/bank-core.ts @@ -17,19 +17,20 @@ import { AmountJson, Amounts, + HttpStatusCode, Logger } from "@gnu-taler/taler-util"; import { + HttpRequestLibrary, createPlatformHttpLib, expectSuccessResponseOrThrow, - HttpRequestLibrary, readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; -import { AccessToken, codecForAccountData, codecForBankAccountCreateWithdrawalResponse, codecForBankAccountGetWithdrawalResponse, codecForBankAccountTransactionInfo, codecForBankAccountTransactionsResponse, codecForCashoutConversionResponse, codecForCashoutPending, codecForCashouts, codecForCashoutStatusResponse, codecForConversionRatesResponse, codecForCoreBankConfig, codecForGlobalCashouts, codecForListBankAccountsResponse, codecForMonitorResponse, codecForPublicAccountsResponse, codecForTokenSuccessResponse, TalerAuthentication, TalerCorebankApi } from "./types.js"; -import { addPaginationParams, makeBasicAuthHeader, makeBearerTokenAuthHeader, PaginationParams, UserAndPassword, UserAndToken } from "./utils.js"; +import { TalerBankIntegrationHttpClient } from "./bank-integration.js"; import { TalerRevenueHttpClient } from "./bank-revenue.js"; import { TalerWireGatewayHttpClient } from "./bank-wire.js"; -import { TalerBankIntegrationHttpClient } from "./bank-integration.js"; +import { AccessToken, TalerAuthentication, TalerCorebankApi, codecForAccountData, codecForBankAccountCreateWithdrawalResponse, codecForBankAccountGetWithdrawalResponse, codecForBankAccountTransactionInfo, codecForBankAccountTransactionsResponse, codecForCashoutConversionResponse, codecForCashoutPending, codecForCashoutStatusResponse, codecForCashouts, codecForConversionRatesResponse, codecForCoreBankConfig, codecForGlobalCashouts, codecForListBankAccountsResponse, codecForMonitorResponse, codecForPublicAccountsResponse, codecForTokenSuccessResponse } from "./types.js"; +import { PaginationParams, UserAndPassword, UserAndToken, addPaginationParams, httpEmptySuccess, httpSuccess, knownFailure, makeBasicAuthHeader, makeBearerTokenAuthHeader, unknownFailure } from "./utils.js"; const logger = new Logger("http-client/core-bank.ts"); @@ -80,12 +81,16 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME * */ - async getConfig(): Promise<TalerCorebankApi.Config> { + async getConfig() { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET" }); - return readSuccessResponseJsonOrThrow(resp, codecForCoreBankConfig()); + switch (resp.status) { + //FIXME: missing in docs + case HttpStatusCode.Ok: return httpSuccess(resp, codecForCoreBankConfig()) + default: return unknownFailure(url, resp) + } } // @@ -96,7 +101,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#post--accounts * */ - async createAccount(auth: AccessToken, body: TalerCorebankApi.RegisterAccountRequest): Promise<void> { + async createAccount(auth: AccessToken, body: TalerCorebankApi.RegisterAccountRequest) { const url = new URL(`accounts`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -105,14 +110,25 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth) }, }); - return expectSuccessResponseOrThrow(resp); + switch (resp.status) { + case HttpStatusCode.NoContent: return httpEmptySuccess() + case HttpStatusCode.BadRequest: return knownFailure("invalid-input", resp); + case HttpStatusCode.Forbidden: { + if (body.username === "bank" || body.username === "admin") { + return knownFailure("unable-to-create", resp); + } else { + return knownFailure("unauthorized", resp); + } + } + case HttpStatusCode.Conflict: return knownFailure("already-exist", resp); + default: return unknownFailure(url, resp) + } } - /** * https://docs.taler.net/core/api-corebank.html#delete--accounts-$USERNAME * */ - async deleteAccount(auth: UserAndToken): Promise<void> { + async deleteAccount(auth: UserAndToken) { const url = new URL(`accounts/${auth.username}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "DELETE", @@ -120,14 +136,26 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth.token) }, }); - return expectSuccessResponseOrThrow(resp); + switch (resp.status) { + case HttpStatusCode.NoContent: return httpEmptySuccess() + case HttpStatusCode.NotFound: return knownFailure("not-found", resp); + case HttpStatusCode.Forbidden: { + if (auth.username === "bank" || auth.username === "admin") { + return knownFailure("unable-to-delete", resp); + } else { + return knownFailure("unauthorized", resp); + } + } + case HttpStatusCode.PreconditionFailed: return knownFailure("balance-not-zero", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME * */ - async updateAccount(auth: UserAndToken, body: TalerCorebankApi.AccountReconfiguration): Promise<void> { + async updateAccount(auth: UserAndToken, body: TalerCorebankApi.AccountReconfiguration) { const url = new URL(`accounts/${auth.username}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "PATCH", @@ -136,14 +164,19 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth.token) }, }); - return expectSuccessResponseOrThrow(resp); + switch (resp.status) { + case HttpStatusCode.NoContent: return httpEmptySuccess() + case HttpStatusCode.NotFound: return knownFailure("not-found", resp); + case HttpStatusCode.Forbidden: return knownFailure("unauthorized", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME-auth * */ - async updatePassword(auth: UserAndToken, body: TalerCorebankApi.AccountPasswordChange): Promise<void> { + async updatePassword(auth: UserAndToken, body: TalerCorebankApi.AccountPasswordChange) { const url = new URL(`accounts/${auth.username}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "PATCH", @@ -152,28 +185,41 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth.token) }, }); - return expectSuccessResponseOrThrow(resp); + switch (resp.status) { + case HttpStatusCode.NoContent: return httpEmptySuccess() + //FIXME: missing in docs + case HttpStatusCode.NotFound: return knownFailure("not-found", resp); + //FIXME: missing in docs + case HttpStatusCode.Forbidden: return knownFailure("unauthorized", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/get-$BANK_API_BASE_URL-public-accounts * */ - async getPublicAccounts(): Promise<TalerCorebankApi.PublicAccountsResponse> { + async getPublicAccounts() { const url = new URL(`public-accounts`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { }, }); - return readSuccessResponseJsonOrThrow(resp, codecForPublicAccountsResponse()); + switch (resp.status) { + //FIXME: missing in docs + case HttpStatusCode.Ok: return httpSuccess(resp, codecForPublicAccountsResponse()) + //FIXME: missing in docs + case HttpStatusCode.NoContent: return httpEmptySuccess() + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#get--accounts * */ - async getAccounts(auth: AccessToken): Promise<TalerCorebankApi.ListBankAccountsResponse> { + async getAccounts(auth: AccessToken) { const url = new URL(`accounts`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -181,14 +227,19 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth) }, }); - return readSuccessResponseJsonOrThrow(resp, codecForListBankAccountsResponse()); + switch (resp.status) { + case HttpStatusCode.Ok: return httpSuccess(resp, codecForListBankAccountsResponse()) + case HttpStatusCode.NoContent: return httpEmptySuccess() + case HttpStatusCode.Forbidden: return knownFailure("unauthorized", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME * */ - async getAccount(auth: UserAndToken): Promise<TalerCorebankApi.AccountData> { + async getAccount(auth: UserAndToken) { const url = new URL(`accounts/${auth.username}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -196,7 +247,14 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth.token) }, }); - return readSuccessResponseJsonOrThrow(resp, codecForAccountData()); + switch (resp.status) { + case HttpStatusCode.Ok: return httpSuccess(resp, codecForAccountData()) + //FIXME: missing in docs + case HttpStatusCode.NotFound: return knownFailure("not-found", resp); + //FIXME: missing in docs + case HttpStatusCode.Forbidden: return knownFailure("unauthorized", resp); + default: return unknownFailure(url, resp) + } } // @@ -207,7 +265,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#get-$BANK_API_BASE_URL-accounts-$account_name-transactions * */ - async getTransactions(auth: UserAndToken, pagination?: PaginationParams): Promise<TalerCorebankApi.BankAccountTransactionsResponse> { + async getTransactions(auth: UserAndToken, pagination?: PaginationParams) { const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl); addPaginationParams(url, pagination) const resp = await this.httpLib.fetch(url.href, { @@ -216,14 +274,23 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth.token) }, }); - return readSuccessResponseJsonOrThrow(resp, codecForBankAccountTransactionsResponse()); + switch (resp.status) { + case HttpStatusCode.Ok: return httpSuccess(resp, codecForBankAccountTransactionsResponse()) + //FIXME: missing in docs + case HttpStatusCode.NoContent: return httpEmptySuccess() + //FIXME: missing in docs + case HttpStatusCode.NotFound: return knownFailure("not-found", resp); + //FIXME: missing in docs + case HttpStatusCode.Forbidden: return knownFailure("unauthorized", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#get-$BANK_API_BASE_URL-accounts-$account_name-transactions-$transaction_id * */ - async getTransactionById(auth: UserAndToken, txid: number): Promise<TalerCorebankApi.BankAccountTransactionInfo> { + async getTransactionById(auth: UserAndToken, txid: number) { const url = new URL(`accounts/${auth.username}/transactions/${String(txid)}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -231,14 +298,21 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth.token) }, }); - return readSuccessResponseJsonOrThrow(resp, codecForBankAccountTransactionInfo()); + switch (resp.status) { + case HttpStatusCode.Ok: return httpSuccess(resp, codecForBankAccountTransactionInfo()) + //FIXME: missing in docs + case HttpStatusCode.NotFound: return knownFailure("not-found", resp); + //FIXME: missing in docs + case HttpStatusCode.Forbidden: return knownFailure("unauthorized", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-accounts-$account_name-transactions * */ - async createTransaction(auth: UserAndToken, body: TalerCorebankApi.CreateBankAccountTransactionCreate): Promise<void> { + async createTransaction(auth: UserAndToken, body: TalerCorebankApi.CreateBankAccountTransactionCreate) { const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -247,7 +321,15 @@ export class TalerCoreBankHttpClient { }, body, }); - return expectSuccessResponseOrThrow(resp); + switch (resp.status) { + //FIXME: fix docs... it should be NoContent + case HttpStatusCode.Ok: return httpEmptySuccess() + case HttpStatusCode.NoContent: return httpEmptySuccess() + case HttpStatusCode.BadRequest: return knownFailure("invalid-input", resp); + //FIXME: missing in docs + case HttpStatusCode.Forbidden: return knownFailure("unauthorized", resp); + default: return unknownFailure(url, resp) + } } // @@ -258,7 +340,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-accounts-$account_name-withdrawals * */ - async createWithdrawal(auth: UserAndToken, body: TalerCorebankApi.BankAccountCreateWithdrawalRequest): Promise<TalerCorebankApi.BankAccountCreateWithdrawalResponse> { + async createWithdrawal(auth: UserAndToken, body: TalerCorebankApi.BankAccountCreateWithdrawalRequest) { const url = new URL(`accounts/${auth.username}/withdrawals`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -267,43 +349,65 @@ export class TalerCoreBankHttpClient { }, body, }); - return readSuccessResponseJsonOrThrow(resp, codecForBankAccountCreateWithdrawalResponse()); + switch (resp.status) { + //FIXME: missing in docs + case HttpStatusCode.Ok: return httpSuccess(resp, codecForBankAccountCreateWithdrawalResponse()) + case HttpStatusCode.Forbidden: return knownFailure("insufficient-funds", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-accounts-$account_name-withdrawals * */ - async getWithdrawalById(wid: string): Promise<TalerCorebankApi.BankAccountGetWithdrawalResponse> { + async getWithdrawalById(wid: string) { const url = new URL(`withdrawals/${wid}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", }); - return readSuccessResponseJsonOrThrow(resp, codecForBankAccountGetWithdrawalResponse()); + switch (resp.status) { + //FIXME: missing in docs + case HttpStatusCode.Ok: return httpSuccess(resp, codecForBankAccountGetWithdrawalResponse()) + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-withdrawals-$withdrawal_id-abort * */ - async abortWithdrawalById(wid: string): Promise<void> { + async abortWithdrawalById(wid: string) { const url = new URL(`withdrawals/${wid}/abort`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", }); - return expectSuccessResponseOrThrow(resp); + switch (resp.status) { + //FIXME: fix docs... it should be NoContent + case HttpStatusCode.Ok: return httpEmptySuccess() + case HttpStatusCode.NoContent: return httpEmptySuccess() + case HttpStatusCode.Conflict: return knownFailure("previously-confirmed", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#post-$BANK_API_BASE_URL-withdrawals-$withdrawal_id-confirm * */ - async confirmWithdrawalById(wid: string): Promise<void> { + async confirmWithdrawalById(wid: string) { const url = new URL(`withdrawals/${wid}/confirm`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", }); - return expectSuccessResponseOrThrow(resp); + switch (resp.status) { + //FIXME: fix docs... it should be NoContent + case HttpStatusCode.Ok: return httpEmptySuccess() + case HttpStatusCode.NoContent: return httpEmptySuccess() + case HttpStatusCode.Conflict: return knownFailure("previously-aborted", resp); + case HttpStatusCode.UnprocessableEntity: return knownFailure("no-exchange-or-reserve-selected", resp); + default: return unknownFailure(url, resp) + } } // @@ -314,7 +418,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts * */ - async createCashout(auth: UserAndToken, body: TalerCorebankApi.CashoutRequest): Promise<TalerCorebankApi.CashoutPending> { + async createCashout(auth: UserAndToken, body: TalerCorebankApi.CashoutRequest) { const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -323,14 +427,20 @@ export class TalerCoreBankHttpClient { }, body, }); - return readSuccessResponseJsonOrThrow(resp, codecForCashoutPending()); + switch (resp.status) { + case HttpStatusCode.Accepted: return httpSuccess(resp, codecForCashoutPending()) + //FIXME: it should be precondition-failed + case HttpStatusCode.Conflict: return knownFailure("invalid-state", resp); + case HttpStatusCode.ServiceUnavailable: return knownFailure("tan-not-supported", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-abort * */ - async abortCashoutById(auth: UserAndToken, cid: string): Promise<void> { + async abortCashoutById(auth: UserAndToken, cid: string) { const url = new URL(`accounts/${auth.username}/cashouts/${cid}/abort`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -338,14 +448,19 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth.token) }, }); - return expectSuccessResponseOrThrow(resp); + switch (resp.status) { + case HttpStatusCode.NoContent: return httpEmptySuccess() + case HttpStatusCode.NotFound: return knownFailure("not-found", resp); + case HttpStatusCode.Conflict: return knownFailure("already-confirmed", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-confirm * */ - async confirmCashoutById(auth: UserAndToken, cid: string, body: TalerCorebankApi.CashoutConfirmRequest): Promise<void> { + async confirmCashoutById(auth: UserAndToken, cid: string, body: TalerCorebankApi.CashoutConfirmRequest) { const url = new URL(`accounts/${auth.username}/cashouts/${cid}/confirm`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -354,14 +469,20 @@ export class TalerCoreBankHttpClient { }, body, }); - return expectSuccessResponseOrThrow(resp); + switch (resp.status) { + case HttpStatusCode.NoContent: return httpEmptySuccess() + case HttpStatusCode.Forbidden: return knownFailure("wrong-tan-or-credential", resp); + case HttpStatusCode.NotFound: return knownFailure("not-found", resp); + case HttpStatusCode.Conflict: return knownFailure("cashout-address-changed", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-confirm * */ - async getCashoutRate(conversion: { debit?: AmountJson, credit?: AmountJson }): Promise<TalerCorebankApi.CashoutConversionResponse> { + async getCashoutRate(conversion: { debit?: AmountJson, credit?: AmountJson }) { const url = new URL(`cashout-rate`, this.baseUrl); if (conversion.debit) { url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit)) @@ -372,14 +493,19 @@ export class TalerCoreBankHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "GET", }); - return readSuccessResponseJsonOrThrow(resp, codecForCashoutConversionResponse()); + switch (resp.status) { + case HttpStatusCode.Ok: return httpSuccess(resp, codecForCashoutConversionResponse()) + case HttpStatusCode.BadRequest: return knownFailure("wrong-calculation", resp); + case HttpStatusCode.NotFound: return knownFailure("not-supported", resp); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts * */ - async getAccountCashouts(auth: UserAndToken): Promise<TalerCorebankApi.Cashouts> { + async getAccountCashouts(auth: UserAndToken) { const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -387,14 +513,18 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth.token) }, }); - return readSuccessResponseJsonOrThrow(resp, codecForCashouts()); + switch (resp.status) { + case HttpStatusCode.Ok: return httpSuccess(resp, codecForCashouts()) + case HttpStatusCode.NoContent: return httpEmptySuccess(); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#get--cashouts * */ - async getGlobalCashouts(auth: AccessToken): Promise<TalerCorebankApi.GlobalCashouts> { + async getGlobalCashouts(auth: AccessToken) { const url = new URL(`cashouts`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -402,14 +532,18 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth) }, }); - return readSuccessResponseJsonOrThrow(resp, codecForGlobalCashouts()); + switch (resp.status) { + case HttpStatusCode.Ok: return httpSuccess(resp, codecForGlobalCashouts()) + case HttpStatusCode.NoContent: return httpEmptySuccess(); + default: return unknownFailure(url, resp) + } } /** * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts-$CASHOUT_ID * */ - async getCashoutById(auth: UserAndToken, cid: string): Promise<TalerCorebankApi.CashoutStatusResponse> { + async getCashoutById(auth: UserAndToken, cid: string) { const url = new URL(`accounts/${auth.username}/cashouts/${cid}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -417,7 +551,12 @@ export class TalerCoreBankHttpClient { Authorization: makeBearerTokenAuthHeader(auth.token) }, }); - return readSuccessResponseJsonOrThrow(resp, codecForCashoutStatusResponse()); + switch (resp.status) { + //FIXME: missing in docs + case HttpStatusCode.Ok: return httpSuccess(resp, codecForCashoutStatusResponse()) + case HttpStatusCode.NotFound: return knownFailure("already-aborted", resp); + default: return unknownFailure(url, resp) + } } // @@ -428,12 +567,16 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#get--conversion-rates * */ - async getConversionRates(): Promise<TalerCorebankApi.ConversionRatesResponse> { + async getConversionRates() { const url = new URL(`conversion-rates`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", }); - return readSuccessResponseJsonOrThrow(resp, codecForConversionRatesResponse()); + switch (resp.status) { + case HttpStatusCode.Ok: return httpSuccess(resp, codecForConversionRatesResponse()) + case HttpStatusCode.NotFound: return knownFailure("not-supported", resp); + default: return unknownFailure(url, resp) + } } // @@ -444,14 +587,19 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#get--monitor * */ - async getMonitor(params: { timeframe: TalerCorebankApi.MonitorTimeframeParam, which: number }): Promise<TalerCorebankApi.MonitorResponse> { + async getMonitor(params: { timeframe: TalerCorebankApi.MonitorTimeframeParam, which: number }) { const url = new URL(`monitor`, this.baseUrl); url.searchParams.set("timeframe", params.timeframe.toString()) url.searchParams.set("which", String(params.which)) const resp = await this.httpLib.fetch(url.href, { method: "GET", }); - return readSuccessResponseJsonOrThrow(resp, codecForMonitorResponse()); + switch (resp.status) { + case HttpStatusCode.Ok: return httpSuccess(resp, codecForMonitorResponse()) + case HttpStatusCode.NotFound: return knownFailure("not-supported", resp); + case HttpStatusCode.BadRequest: return knownFailure("invalid-input", resp); + default: return unknownFailure(url, resp) + } } // diff --git a/packages/taler-util/src/http-client/index.ts b/packages/taler-util/src/http-client/index.ts deleted file mode 100644 index e69de29bb..000000000 --- a/packages/taler-util/src/http-client/index.ts +++ /dev/null diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts index 4588f945c..f4af5ae03 100644 --- a/packages/taler-util/src/http-client/utils.ts +++ b/packages/taler-util/src/http-client/utils.ts @@ -1,6 +1,10 @@ import { base64FromArrayBuffer } from "../base64.js"; +import { HttpResponse, readErrorResponse, readSuccessResponseJsonOrThrow, readTalerErrorResponse } from "../http-common.js"; +import { HttpStatusCode } from "../http-status-codes.js"; +import { Codec } from "../index.js"; import { stringToBytes } from "../taler-crypto.js"; -import { AccessToken, TalerAuthentication } from "./types.js"; +import { TalerErrorDetail } from "../wallet-types.js"; +import { AccessToken } from "./types.js"; /** * Helper function to generate the "Authorization" HTTP header. @@ -66,3 +70,62 @@ export type PaginationParams = { */ order: "asc" | "dec" } + +export type HttpResult<Body, ErrorEnum> = + | HttpOk<Body> + | HttpKnownFail<ErrorEnum> + | HttpUnkownFail; + +/** + * 200 < status < 204 + */ +export interface HttpOk<T> { + type: "ok", + body: T; +} + +/** + * 400 < status < 409 + * and error documented + */ +export interface HttpKnownFail<T> { + type: "fail", + case: T, + detail: TalerErrorDetail, +} + +/** + * 400 < status < 599 + * and error NOT documented + * undefined behavior on this responses + */ +export interface HttpUnkownFail { + type: "fail-unknown", + url: URL; + status: HttpStatusCode; + + // read from the body if exist + detail?: TalerErrorDetail; + body?: string; +} + +export async function knownFailure<T extends string>(s: T, resp: HttpResponse): Promise<HttpKnownFail<T>> { + const detail = await readTalerErrorResponse(resp) + return { type: "fail", case: s, detail } +} +export async function httpSuccess<T>(resp: HttpResponse, codec: Codec<T>): Promise<HttpOk<T>> { + const body = await readSuccessResponseJsonOrThrow(resp, codec) + return { type: "ok" as const, body } +} +export function httpEmptySuccess(): HttpOk<void> { + return { type: "ok" as const, body: void 0 } +} +export async function unknownFailure(url: URL, resp: HttpResponse): Promise<HttpUnkownFail> { + if (resp.status >= 400 && resp.status < 500) { + const detail = await readTalerErrorResponse(resp) + return { type: "fail-unknown", url, status: resp.status, detail } + } else { + const { detail, body } = await readErrorResponse(resp) + return { type: "fail-unknown", url, status: resp.status, detail, body } + } +} diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts index da2fbb9da..817f2367f 100644 --- a/packages/taler-util/src/http-common.ts +++ b/packages/taler-util/src/http-common.ts @@ -180,6 +180,37 @@ export async function readTalerErrorResponse( return errJson; } +export async function readErrorResponse( + httpResponse: HttpResponse, +): Promise<{ detail: TalerErrorDetail | undefined, body: string }> { + let errString: string; + try { + errString = await httpResponse.text(); + } catch (e: any) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + { + requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, + httpStatusCode: httpResponse.status, + validationError: e.toString(), + }, + "Couldn't parse JSON format from error response", + ); + } + let errJson; + try { + errJson = JSON.parse(errString) + } catch (e) { + errJson = undefined + } + + const talerErrorCode = errJson && errJson.code; + if (typeof talerErrorCode === "number") { + return { detail: errJson, body: errString } + } + return { detail: undefined, body: errString }; +} export async function readUnexpectedResponseDetails( httpResponse: HttpResponse, ): Promise<TalerErrorDetail> { |