commit 25fe7beae8fd18b008f4a014a7655be37fd0ccb9
parent 5c4f580867b6c6852d291a5e52949cedf71e995e
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:47 +0200
wallet-core: do not retry template order creation
Diffstat:
2 files changed, 68 insertions(+), 10 deletions(-)
diff --git a/packages/taler-wallet-core/src/pay-template.test.ts b/packages/taler-wallet-core/src/pay-template.test.ts
@@ -16,6 +16,7 @@
import {
AmountString,
+ CancellationToken,
HostPortPath,
TalerPayTemplateUri,
TalerUriAction,
@@ -25,7 +26,12 @@ import {
} from "@gnu-taler/taler-util";
import assert from "node:assert";
import { test } from "node:test";
-import { applyTemplateUriOverrides } from "./pay-template.js";
+import {
+ applyTemplateUriOverrides,
+ instantiateTemplateRaw,
+} from "./pay-template.js";
+import { ProgressContext } from "./progress.js";
+import { WalletExecutionContext } from "./wallet.js";
function paivanaUri(amount: AmountString): TalerPayTemplateUri {
return {
@@ -84,3 +90,56 @@ test("a fixed paivana amount leaves other currencies and complex choices alone",
["EUR:5", "KUDOS:5"],
);
});
+
+test("template order creation is not retried after an ambiguous failure", async () => {
+ const cts = CancellationToken.create();
+ let requestCount = 0;
+ const progressContext: ProgressContext = {
+ operation: "preparePayForTemplateV2",
+ progressToken: "progress-token",
+ finished: false,
+ cts,
+ progressContextId: 1,
+ };
+ const wex = {
+ cancellationToken: cts.token,
+ progressContext,
+ http: {
+ async fetch(): Promise<never> {
+ requestCount++;
+ if (requestCount > 1) {
+ cts.cancel();
+ }
+ throw Error("connection lost after sending request");
+ },
+ },
+ ws: {
+ notify(): void {},
+ timerGroup: {
+ async resolveAfter(): Promise<boolean> {
+ return true;
+ },
+ },
+ },
+ } as unknown as WalletExecutionContext;
+
+ await assert.rejects(
+ instantiateTemplateRaw(
+ wex,
+ {
+ talerPayTemplateUri:
+ "taler+http://pay-template/merchant.example.com:1234/template-id",
+ progressToken: "progress-token",
+ },
+ {
+ template_contract: {
+ template_type: TemplateType.FIXED_ORDER,
+ amount: "TESTKUDOS:1",
+ summary: "test order",
+ },
+ },
+ ),
+ /connection lost/,
+ );
+ assert.strictEqual(requestCount, 1);
+});
diff --git a/packages/taler-wallet-core/src/pay-template.ts b/packages/taler-wallet-core/src/pay-template.ts
@@ -325,15 +325,14 @@ export async function instantiateTemplateRaw(
req.templateParams ?? {},
);
- // Retried under a progress context. Instantiating a template creates a
- // fresh order server-side, so a retry after a failure just starts another
- // one; the order the wallet ends up paying is the one this call returns.
- const resp = await runWithProgressRetries(wex, async () =>
- succeedOrThrow(
- await merchantApi.useTemplateCreateOrder(
- parsedUri.templateId,
- templateDetails,
- ),
+ // Instantiating a template is not idempotent. In particular, a transport
+ // failure can happen after the merchant committed the new order. Retrying
+ // here would create an additional order with no way to identify the first
+ // one, so this POST is attempted exactly once.
+ const resp = succeedOrThrow(
+ await merchantApi.useTemplateCreateOrder(
+ parsedUri.templateId,
+ templateDetails,
),
);