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

  1. Complete the payment method setup flow (e.g. IDEAL redirect/confirmation) first, then call setDefaultPaymentMethod
  2. Treat the 409 as a signal to poll/re-fetch the payment method status until setup completes
  3. 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

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


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)