signalapp/Signal-Server · error · IllegalArgumentException

Invalid action:

Error message

Invalid action: 

What it means

DeviceCheckController.Action.fromString converts a request-supplied string into the Action enum by case-insensitive name comparison; if no enum constant matches it throws IllegalArgumentException("Invalid action: " + action). It is a strict input-validation guard for the action query/body parameter.

Solutions

  1. Send one of the exact supported action values (case-insensitive match on the enum names, e.g. 'attest' or 'assert')
  2. Trim and normalize the action string on the client before sending
  3. Check the server's DeviceCheckController.Action enum for the list of valid values for your server version
  4. Upgrade the client or server if a newly added action is missing from one side

Example fix

// before
client.deviceCheckAction("attestation"); // IllegalArgumentException: Invalid action: attestation
// after
client.deviceCheckAction("attest"); // matches Action.ATTEST via fromString
Defensive patterns

Strategy: validation

Validate before calling

final Set<String> valid = Arrays.stream(Action.values()).map(a -> a.name().toLowerCase(Locale.ROOT)).collect(Collectors.toSet());
if (!valid.contains(action.trim().toLowerCase(Locale.ROOT))) {
  throw new IllegalArgumentException("action must be one of " + valid);
}

Try / catch

try {
  client.deviceCheckAction(action);
} catch (WebApplicationException | IllegalArgumentException e) {
  if (String.valueOf(e).contains("Invalid action")) {
    // fall back to a supported action or surface a config error
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the devicecheck endpoint with an action parameter that is not exactly one of the Action enum names (case-insensitive), e.g. 'attestation' instead of 'attest', a typo, trailing whitespace, or an empty string.

Common situations: API consumers guessing action values instead of using documented enum names; version drift where the client sends an action the server build does not define; URL encoding or whitespace corruption of the parameter.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckController.java:273

    // The request assertion was validated, execute it
    switch (request.assertionRequest().action()) {
      case BACKUP -> backupAuthManager.extendBackupVoucher(
              account,
              new Account.BackupVoucher(BackupLevel.PAID.getValue(), clock.instant().plus(backupRedemptionDuration)));
    }
  }

  public enum Action {
    BACKUP;

    @JsonCreator
    public static Action fromString(final String action) {
      for (final Action a : Action.values()) {
        if (a.name().toLowerCase(Locale.ROOT).equals(action)) {
          return a;
        }
      }
      throw new IllegalArgumentException("Invalid action: " + action);
    }
  }

  public record AssertionRequest(
      @Schema(description = "The challenge retrieved at `GET /v1/devicecheck/assert`")
      String challenge,
      @Schema(description = "The type of action you'd like to perform with this assert",
          allowableValues = {"backup"}, implementation = String.class)
      Action action) {}

  /*
   * Parses the base64 encoded AssertionRequest, but preserves the rawJson as well
   */
  public record AssertionRequestWrapper(AssertionRequest assertionRequest, byte[] rawJson) {

    public static AssertionRequestWrapper fromString(String requestBase64) throws IOException {
      final byte[] requestJson = Base64.getUrlDecoder().decode(requestBase64);
      final AssertionRequest requestData = SystemMapper.jsonMapper().readValue(requestJson, AssertionRequest.class);

View on GitHub (pinned to 100ab61c82)