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
- Reject deviceId == 1 in the API/frontend before calling removeDevice
- Only list secondary devices as removable in the UI
- Guard the call: if (deviceId != Device.PRIMARY_ID) accounts.removeDevice(...)
- 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
- Expose only secondary devices as removable in APIs/UIs
- Validate deviceId > 1 in request DTOs
- Write a unit test asserting primary removal is rejected
- Document PRIMARY_ID semantics in client SDKs
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
- Only primary devices can link devices
- PNI identity key must not be provided if existing account…
- end of range must be after start of range
- cannot use play billing for one-time donations
- cannot use app store purchases for one-time donations
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)