signalapp/Signal-Server · error · IllegalArgumentException

Cannot remove primary device

Error message

Cannot remove primary device

What it means

AccountsManager.removeDevice throws IllegalArgumentException("Cannot remove primary device") when asked to delete device id 1 (Device.PRIMARY_ID). The primary device is the root of an account's device list and can never be unlinked; only linked (secondary) devices may be removed.

Solutions

  1. Reject deviceId == 1 in the API/frontend before calling removeDevice
  2. Only list secondary devices as removable in the UI
  3. Guard the call: if (deviceId != Device.PRIMARY_ID) accounts.removeDevice(...)
  4. Return a 400-style client error instead of propagating the 500-class failure

Example fix

// before
accounts.removeDevice(accountUuid, request.deviceId());
// after
if (request.deviceId() == Device.PRIMARY_ID) {
  throw new WebApplicationException(Response.status(400).build());
}
accounts.removeDevice(accountUuid, request.deviceId());
Defensive patterns

Strategy: validation

Validate before calling

// Guard before calling removeDevice
if (deviceId == Device.PRIMARY_ID) {
  throw new WebApplicationException(Response.status(400).build());
}

Type guard

static boolean isRemovableDevice(byte deviceId) {
  return deviceId != Device.PRIMARY_ID;
}

Try / catch

try {
  accounts.removeDevice(accountUuid, deviceId);
} catch (IllegalArgumentException e) {
  if (e.getMessage().equals("Cannot remove primary device")) {
    return Response.status(400).build();
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling removeDevice(accountIdentifier, deviceId) with deviceId == Device.PRIMARY_ID (1).

Common situations: API clients posting a device-unlink request with id 1; UI bug allowing the primary device row to be selected for removal; off-by-one errors when enumerating device ids.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java:899

    final Instant tokenExpiration = timestamp.plus(LINK_DEVICE_TOKEN_EXPIRATION_DURATION);

    if (tokenExpiration.isBefore(clock.instant())) {
      return Optional.empty();
    }

    return Optional.of(aci);
  }

  /**
   * Unlink a device from the given account. The device will be immediately disconnected if it is connected to any chat
   * frontend.
   *
   * @return the updated Account
   */
  public Account removeDevice(final UUID accountIdentifier, final byte deviceId) {
    if (deviceId == Device.PRIMARY_ID) {
      throw new IllegalArgumentException("Cannot remove primary device");
    }

    // Always fetch a fresh, non-cached copy of the account before making modifications
    final Account account = accounts.getByAccountIdentifier(accountIdentifier)
        .orElseThrow(() -> new IllegalArgumentException("Account not found: " + accountIdentifier));

    return accountLockManager.withSingleAccountLock(account,
        () -> removeDevice(accountIdentifier, deviceId, MAX_UPDATE_ATTEMPTS));
  }

  private Account removeDevice(final UUID accountIdentifier, final byte deviceId, final int retries) {
    final Account account = accounts.getByAccountIdentifier(accountIdentifier)
        .orElseThrow(ContestedOptimisticLockException::new);

    CompletableFuture.allOf(
            keysManager.deleteSingleUsePreKeys(account.getAccountIdentifier(), deviceId),
            account.getPhoneNumberIdentifier()
                .map(pni -> keysManager.deleteSingleUsePreKeys(pni, deviceId))

View on GitHub (pinned to 100ab61c82)