From 3c7b98a0f93537a0405f404515bcaa47c8311ba1 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Thu, 8 Oct 2026 12:55:03 +0200 Subject: [PATCH 1/3] Rotate idempotency key when gotten a 40x error on upload --- .../main/java/eu/pretix/libpretixsync/sync/SyncManager.java | 5 +++++ .../common/eu/pretix/libpretixsync/sqldelight/QueuedCall.sq | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java index 9ed1f294..924e44ab 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java @@ -1,6 +1,7 @@ package eu.pretix.libpretixsync.sync; import eu.pretix.libpretixsync.api.*; +import eu.pretix.libpretixsync.db.NonceGenerator; import eu.pretix.libpretixsync.models.Question; import eu.pretix.libpretixsync.models.db.QuestionExtensionsKt; import eu.pretix.libpretixsync.sqldelight.Closing; @@ -545,6 +546,10 @@ protected void uploadQueuedCalls(ProgressFeedback feedback) throws SyncException sentry.addBreadcrumb("sync.queue", "API Error: " + e.getMessage()); throw new SyncException(e.getMessage()); } + } catch (PermissionDeniedApiException | DeviceAccessRevokedException | UnauthorizedApiException e) { + sentry.addBreadcrumb("sync.queue", "API Error: " + e.getMessage()); + db.getQueuedCallQueries().updateIdempotencyKey(NonceGenerator.nextNonce(), call.getId()); + throw new SyncException(e.getMessage()); } catch (ApiException e) { sentry.addBreadcrumb("sync.queue", "API Error: " + e.getMessage()); throw new SyncException(e.getMessage()); diff --git a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedCall.sq b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedCall.sq index a518d4b9..f16c3af5 100644 --- a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedCall.sq +++ b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedCall.sq @@ -26,3 +26,9 @@ VALUES ( ?, ? ); + +updateIdempotencyKey: +UPDATE QueuedCall +SET + idempotency_key = ? +WHERE id = ?; \ No newline at end of file From 38ea25771d4bcca7b46eab786478ddbba2e239e8 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Thu, 8 Oct 2026 15:17:30 +0200 Subject: [PATCH 2/3] Rotate idempotency key for order upload on 40x error too --- .../libpretixsync/sync/SyncManager.java | 42 ++++++++++++------- .../libpretixsync/sqldelight/QueuedOrder.sq | 6 +++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java index 924e44ab..87e40164 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java @@ -620,9 +620,9 @@ protected void uploadOrders(ProgressFeedback feedback) throws SyncException { List orders = db.getQueuedOrderQueries().selectUnlockedWithoutError().executeAsList(); - try { - int i = 0; - for (QueuedOrder qo : orders) { + int i = 0; + for (QueuedOrder qo : orders) { + try { if (feedback != null && i % 10 == 0) { feedback.postFeedback("Uploading orders (" + i + "/" + orders.size() + ") …"); } @@ -646,19 +646,31 @@ protected void uploadOrders(ProgressFeedback feedback) throws SyncException { // TODO: User feedback or log in some way? db.getQueuedOrderQueries().updateError(resp.getData().toString(), qo.getId()); } + if (resp.getResponse().code() >= 400 && resp.getResponse().code() <= 500) { + db.getQueuedOrderQueries().updateIdempotencyKey(NonceGenerator.nextNonce(), qo.getId()); + } + } catch (JSONException e) { + if (connectivityFeedback != null) { + connectivityFeedback.recordError(); + } + sentry.captureException(e); + throw new SyncException("Unknown server response: " + e.getMessage()); + } catch (ApiException e) { + if (connectivityFeedback != null) { + connectivityFeedback.recordError(); + } + sentry.addBreadcrumb("sync.queue", "API Error: " + e.getMessage()); + if (e instanceof DeviceAccessRevokedException // 401 + || e instanceof UnauthorizedApiException // 401 + || e instanceof PermissionDeniedApiException // 403 + || e instanceof NotFoundApiException // 404 + || e instanceof FinalApiException // >= 405 + || e instanceof ConflictApiException // 409 + ) { + db.getQueuedOrderQueries().updateIdempotencyKey(NonceGenerator.nextNonce(), qo.getId()); + } + throw new SyncException(e.getMessage()); } - } catch (JSONException e) { - if (connectivityFeedback != null) { - connectivityFeedback.recordError(); - } - sentry.captureException(e); - throw new SyncException("Unknown server response: " + e.getMessage()); - } catch (ApiException e) { - if (connectivityFeedback != null) { - connectivityFeedback.recordError(); - } - sentry.addBreadcrumb("sync.queue", "API Error: " + e.getMessage()); - throw new SyncException(e.getMessage()); } sentry.addBreadcrumb("sync.queue", "Receipt upload complete"); diff --git a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedOrder.sq b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedOrder.sq index a50afd55..6ece2b0d 100644 --- a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedOrder.sq +++ b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/QueuedOrder.sq @@ -36,3 +36,9 @@ UPDATE QueuedOrder SET error = ? WHERE id = ?; + +updateIdempotencyKey: +UPDATE QueuedOrder +SET + idempotency_key = ? +WHERE id = ?; \ No newline at end of file From 7afc9ecc4fe80d1a56c597b56c6b0fcb50e98ff7 Mon Sep 17 00:00:00 2001 From: Maximilian Richt Date: Thu, 8 Oct 2026 15:18:16 +0200 Subject: [PATCH 3/3] For queued calls, update idempotency key on more 40x errors --- .../pretix/libpretixsync/sync/SyncManager.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java index 87e40164..325e0043 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java @@ -527,10 +527,12 @@ protected void uploadQueuedCalls(ProgressFeedback feedback) throws SyncException call.getIdempotency_key() ); if (response.getResponse().code() < 500) { - db.getQueuedCallQueries().delete(call.getId()); if (response.getResponse().code() >= 400) { sentry.captureException(new ApiException("Received response (" + response.getResponse().code() + ") for queued call: " + response.getData().toString())); // We ignore 400s, because we can't do something about them + db.getQueuedCallQueries().updateIdempotencyKey(NonceGenerator.nextNonce(), call.getId()); + } else { + db.getQueuedCallQueries().delete(call.getId()); } } else { throw new SyncException(response.getData().toString()); @@ -546,12 +548,17 @@ protected void uploadQueuedCalls(ProgressFeedback feedback) throws SyncException sentry.addBreadcrumb("sync.queue", "API Error: " + e.getMessage()); throw new SyncException(e.getMessage()); } - } catch (PermissionDeniedApiException | DeviceAccessRevokedException | UnauthorizedApiException e) { - sentry.addBreadcrumb("sync.queue", "API Error: " + e.getMessage()); - db.getQueuedCallQueries().updateIdempotencyKey(NonceGenerator.nextNonce(), call.getId()); - throw new SyncException(e.getMessage()); } catch (ApiException e) { sentry.addBreadcrumb("sync.queue", "API Error: " + e.getMessage()); + if (e instanceof DeviceAccessRevokedException // 401 + || e instanceof UnauthorizedApiException // 401 + || e instanceof PermissionDeniedApiException // 403 + || e instanceof NotFoundApiException // 404 + || e instanceof FinalApiException // >= 405 + || e instanceof ConflictApiException // 409 + ) { + db.getQueuedCallQueries().updateIdempotencyKey(NonceGenerator.nextNonce(), call.getId()); + } throw new SyncException(e.getMessage()); } }