signalapp/Signal-Server · warning · ClientErrorException
409 Conflict
Error message
409 Conflict
What it means
setDefaultPaymentMethod catches SubscriptionInvalidArgumentsException and converts it to an HTTP 409 Conflict: an "invalid argument" here means the client issued requests out of order and the payment method (e.g. a Stripe SetupIntent) has not finished setup yet, so it cannot be set as default.
Solutions
- Complete the payment method setup flow (e.g. IDEAL redirect/confirmation) first, then call setDefaultPaymentMethod
- Treat the 409 as a signal to poll/re-fetch the payment method status until setup completes
- Re-run the create_payment_method flow if the setup session expired
Example fix
// before await setDefaultPaymentMethod(token); // after await completeIdealRedirect(); await waitForPaymentMethodSetup(paymentMethodId); await setDefaultPaymentMethod(token);
Defensive patterns
Strategy: retry
Validate before calling
// verify the payment method finished setup before making it default const pm = await getPaymentMethod(paymentMethodId); if (pm.status !== 'READY') await completeSetupFlow(pm);
Try / catch
try { await setDefaultPaymentMethod(token); } catch (e) { if (e.status === 409) { await finishPaymentMethodSetup(paymentMethodId); await setDefaultPaymentMethod(token); } else { throw e; } } Prevention
- Complete any IDEAL/redirect confirmation before calling setDefaultPaymentMethod
- Treat 409 as 'finish setup first', not as a permanent failure
When it happens
Trigger: Calling setDefaultPaymentMethod/setDefaultPaymentMethodForIdeal with a paymentMethodId whose setup (redirect confirmation, mandate, SCA) is incomplete.
Common situations: IDEAL/bank-debit flows where the client must complete a redirect before attaching; retrying default-payment calls before the webhook/confirmation lands.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- cannot create payment methods with payment type
- The PAYPAL payment type must use…
- Invalid payment method
- Operation cannot be performed with the
- 409 Conflict (session already verified)
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/99c1e02881790737.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java:810
}
private void setDefaultPaymentMethod(final CustomerAwareSubscriptionPaymentProcessor manager,
final String paymentMethodId,
final SubscriberCredentials requestData) throws SubscriptionException, IOException {
try {
final Subscriptions.Record record = subscriptionManager.getSubscriber(requestData);
final ProcessorCustomer processorCustomer = record.getProcessorCustomer()
// a missing customer ID indicates the client made requests out of order,
// and needs to call create_payment_method to create a customer for the given payment method
.orElseThrow(() ->new ClientErrorException(Status.CONFLICT));
manager
.setDefaultPaymentMethodForCustomer(processorCustomer.customerId(), paymentMethodId, record.subscriptionId);
} catch (final SubscriptionInvalidArgumentsException e) {
// Here, invalid arguments must mean that the client has made requests out of order, and needs to finish
// setting up the paymentMethod first
throw new ClientErrorException(Status.CONFLICT);
}
}
private String getSubscriptionTemplateId(long level, String currency, PaymentProvider processor) {
final SubscriptionLevelConfiguration config = subscriptionConfiguration.getSubscriptionLevel(level);
if (config == null) {
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
.entity(new SetSubscriptionLevelErrorResponse(List.of(
new SetSubscriptionLevelErrorResponse.Error(
SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL, null))))
.build());
}
final Optional<String> templateId = Optional
.ofNullable(config.prices().get(currency.toLowerCase(Locale.ROOT)))
.map(priceConfiguration -> priceConfiguration.processorIds().get(processor));
return templateId.orElseThrow(() -> new BadRequestException(Response.status(Status.BAD_REQUEST)
.entity(new SetSubscriptionLevelErrorResponse(List.of(
new SetSubscriptionLevelErrorResponse.Error(View on GitHub (pinned to 100ab61c82)