signalapp/Signal-Server · error · BadRequestException

400 Bad Request (UNSUPPORTED_LEVEL)

Error message

400 Bad Request (UNSUPPORTED_LEVEL)

What it means

setSubscriptionLevel catches SubscriptionInvalidLevelException and rethrows as a 400 whose error entity carries type UNSUPPORTED_LEVEL: the requested subscription level is not a supported/donatable level in the current configuration.

Solutions

  1. Query the available subscription levels from the server configuration/GET endpoints and pick a supported level
  2. Update the client's level list to match the current server configuration
  3. Catch this 400 and refresh/re-render the level picker for the user

Example fix

// before
{ "level": 7, "currency": "usd" }
// after — use a configured level, e.g.
{ "level": 5, "currency": "usd" }
Defensive patterns

Strategy: try-catch

Validate before calling

// fetch supported levels first
const levels = await getAvailableSubscriptionLevels();
if (!levels.includes(level)) throw new Error(`Unsupported level ${level}`);

Try / catch

try { await setSubscriptionLevel(level, currency); } catch (e) { if (e.errorType === 'UNSUPPORTED_LEVEL') { await refreshTierList(); } else { throw e; } }

Prevention

When it happens

Trigger: POSTing setSubscriptionLevel with a level that has no configured subscription level (e.g. level 0 or a level absent from subscriptionConfiguration).

Common situations: Hardcoded legacy level numbers after the donation tiers changed; UI sending an experimental level; currency-specific levels that don't exist.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/1105b66e071000ca. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java:428

        SubscriberCredentials.process(authenticatedAccount, subscriberId, clock);
    try {
      final Subscriptions.Record record = subscriptionManager.getSubscriber(subscriberCredentials);
      final ProcessorCustomer processorCustomer = record.getProcessorCustomer()
          .orElseThrow(() ->
              // 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
              new ClientErrorException(Status.CONFLICT));

      final String subscriptionTemplateId = getSubscriptionTemplateId(level, currency,
          processorCustomer.processor());

      final CustomerAwareSubscriptionPaymentProcessor manager = getCustomerAwareProcessor(
          processorCustomer.processor());
      subscriptionManager.updateSubscriptionLevelForCustomer(subscriberCredentials, record, manager, level,
          currency, idempotencyKey, subscriptionTemplateId, this::subscriptionsAreSameType);
      return new SetSubscriptionLevelSuccessResponse(level);
    } catch (SubscriptionInvalidLevelException e) {
      throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST)
          .entity(new SubscriptionController.SetSubscriptionLevelErrorResponse(List.of(
              new SubscriptionController.SetSubscriptionLevelErrorResponse.Error(
                  SubscriptionController.SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL,
                  null))))
          .build());
    } catch (SubscriptionPaymentRequiresActionException e) {
      throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST)
          .entity(new SetSubscriptionLevelErrorResponse(List.of(new SetSubscriptionLevelErrorResponse.Error(
              SetSubscriptionLevelErrorResponse.Error.Type.PAYMENT_REQUIRES_ACTION, null))))
          .build());
    } catch (SubscriptionInvalidArgumentsException e) {
      throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST)
          .entity(new SetSubscriptionLevelErrorResponse(List.of(new SetSubscriptionLevelErrorResponse.Error(
              SetSubscriptionLevelErrorResponse.Error.Type.INVALID_ARGUMENTS, e.getMessage()))))
          .build());
    }
  }

View on GitHub (pinned to 100ab61c82)