commit dfdcc1ff64e2a5d5c1b1891728a878d382438315
parent b445019624df3202ff392d8770bd41c13d5e1606
Author: Florian Dold <florian@dold.me>
Date: Tue, 14 Jul 2026 23:14:43 +0200
wallet-core: new tc dev experiment option for subsequent errors on same URL
Diffstat:
2 files changed, 104 insertions(+), 63 deletions(-)
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -77,7 +77,6 @@ import { PeerPushDebitTransactionContext } from "./pay-peer-push-debit.js";
import { RefreshTransactionContext } from "./refresh.js";
import { rematerializeTransactions } from "./transactions.js";
import {
- DevExperimentState,
WalletExecutionContext,
handleAddGlobalCurrencyExchange,
handleRemoveGlobalCurrencyExchange,
@@ -86,6 +85,74 @@ import { WithdrawTransactionContext } from "./withdraw.js";
const logger = new Logger("dev-experiments.ts");
+export interface DevExperimentState {
+ blockRefreshes?: boolean;
+ /** Pretend that exchanges have no fees.*/
+ pretendNoFees?: boolean;
+ /** Pretend exchange has no withdrawable denoms. */
+ pretendNoDenoms?: boolean;
+ pretendPostWopFailed?: boolean;
+ merchantDepositInsufficient?: boolean;
+ /** Map from base URL to faked version for /config or /keys */
+ fakeProtoVer?: Map<
+ string,
+ {
+ fakeVer: string;
+ }
+ >;
+
+ /** Map from base URL to faked version for /config or /keys */
+ fakeMasterPub?: Map<
+ string,
+ {
+ fakeMasterPub: string;
+ }
+ >;
+
+ fakeShoppingUrl?: string;
+
+ flagDisablePeerPayments?: boolean;
+
+ /** Set of exchanges to fake disabled direct deposits for. */
+ flagDisableDirectDeposits?: Set<string>;
+
+ fakeChfBban?: boolean;
+
+ fakeDemoShortcuts?: AmountString[];
+
+ fakeDefaultExchangeDemo?: boolean;
+
+ blockPayResponse?: boolean;
+
+ /** Migration test for confirmPay */
+ flagConfirmPayNoWait?: boolean;
+
+ tc?: {
+ dropRequestProbability?: number;
+ dropResponseProbability?: number;
+ /**
+ * Fake a 500 response *after* the request has been sent
+ * and the real response has been received.
+ */
+ fake500Probability?: number;
+ /**
+ * Fake a 500 for the number of specified times
+ * for the same URL.
+ *
+ * The counter is reset whenever the star-tc dev experiment
+ * is re-scanned.
+ */
+ fake500Count?: number;
+ delayResponseMs?: number;
+
+ /**
+ * Mapping from $METHOD:$URL to a counter indicating
+ * how many times we faked a 500 response.
+ */
+ fake500Map: Map<string, number>;
+ };
+}
+
/**
* Apply a dev experiment to the wallet database / state.
*/
@@ -183,11 +250,23 @@ export async function applyDevExperiment(
}
throw Error("unsupported parameter");
};
+ const maybeIntParam = (name: string) => {
+ const val = parsedUri.query?.[name];
+ if (val == null) {
+ return undefined;
+ }
+ if (typeof val === "string") {
+ return Number.parseInt(val, 10);
+ }
+ throw Error("unsupported parameter");
+ };
wex.ws.devExperimentState.tc = {
delayResponseMs: maybeFloatParam("delay_resp"),
dropRequestProbability: maybeFloatParam("drop_req"),
dropResponseProbability: maybeFloatParam("drop_resp"),
fake500Probability: maybeFloatParam("fake_500"),
+ fake500Count: maybeIntParam("fake_500_count"),
+ fake500Map: new Map(),
};
return;
}
@@ -877,7 +956,7 @@ export class DevExperimentHttpLib implements HttpRequestLibrary {
`Request timed out after ${5000} ms`,
);
}
- const resp = await this.underlyingLib.fetch(url, opt);
+ let resp = await this.underlyingLib.fetch(url, opt);
if (evalFailProbability(this.devExperimentState.tc?.fake500Probability)) {
return mockInternalServerError(resp);
}
@@ -895,12 +974,28 @@ export class DevExperimentHttpLib implements HttpRequestLibrary {
`Request timed out after ${5000} ms`,
);
}
- // Delay response if requested
- {
- const delayMs = this.devExperimentState.tc?.delayResponseMs;
- if (delayMs != null) {
- logger.info(`delaying response by ${delayMs}ms`);
- await waitMs(delayMs);
+
+ const tc = this.devExperimentState.tc;
+
+ if (tc) {
+ const fake500Count = tc.fake500Count ?? 0;
+ if (fake500Count > 0) {
+ const reqKey = `${(opt?.method ?? "GET").toUpperCase()}:${url}`;
+ const oldCount = tc.fake500Map.get(reqKey) ?? 0;
+ logger.info(`count ${oldCount}/${fake500Count} for ${reqKey}`);
+ tc.fake500Map.set(reqKey, oldCount + 1);
+ if (oldCount <= fake500Count) {
+ resp = mockInternalServerError(resp);
+ }
+ }
+
+ // Delay response if requested
+ {
+ const delayMs = this.devExperimentState.tc?.delayResponseMs;
+ if (delayMs != null) {
+ logger.info(`delaying response by ${delayMs}ms`);
+ await waitMs(delayMs);
+ }
}
}
return resp;
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -331,7 +331,7 @@ import {
} from "./denominations.js";
import { UnverifiedDenomError } from "./denomSelection.js";
import { checkDepositGroup, createDepositGroup } from "./deposits.js";
-import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
+import { DevExperimentHttpLib, DevExperimentState, applyDevExperiment } from "./dev-experiments.js";
import {
handleGetDonau,
handleGetDonauStatements,
@@ -3187,60 +3187,6 @@ export class Wallet {
}
}
-export interface DevExperimentState {
- blockRefreshes?: boolean;
- /** Pretend that exchanges have no fees.*/
- pretendNoFees?: boolean;
- /** Pretend exchange has no withdrawable denoms. */
- pretendNoDenoms?: boolean;
- pretendPostWopFailed?: boolean;
- merchantDepositInsufficient?: boolean;
- /** Map from base URL to faked version for /config or /keys */
- fakeProtoVer?: Map<
- string,
- {
- fakeVer: string;
- }
- >;
-
- /** Map from base URL to faked version for /config or /keys */
- fakeMasterPub?: Map<
- string,
- {
- fakeMasterPub: string;
- }
- >;
-
- fakeShoppingUrl?: string;
-
- flagDisablePeerPayments?: boolean;
-
- /** Set of exchanges to fake disabled direct deposits for. */
- flagDisableDirectDeposits?: Set<string>;
-
- fakeChfBban?: boolean;
-
- fakeDemoShortcuts?: AmountString[];
-
- fakeDefaultExchangeDemo?: boolean;
-
- blockPayResponse?: boolean;
-
- /** Migration test for confirmPay */
- flagConfirmPayNoWait?: boolean;
-
- tc?: {
- dropRequestProbability?: number;
- dropResponseProbability?: number;
- /**
- * Fake a 500 response *after* the request has been sent
- * and the real response has been received.
- */
- fake500Probability?: number;
- delayResponseMs?: number;
- };
-}
-
export class Cache<T> {
private map: Map<string, [AbsoluteTime, T]> = new Map();