signalapp/Signal-Server · error · IllegalArgumentException
Only primary devices can link devices
Error message
Only primary devices can link devices
What it means
AccountsManager.waitForNewLinkedDevice throws IllegalArgumentException("Only primary devices can link devices") when the device performing the link is not the primary device. Linked (secondary) devices are not permitted to authorize additional device links; only the primary can approve new linkings.
Solutions
- Ensure the link-device request is handled by the primary device's session/auth context
- Verify device.isPrimary() before invoking waitForNewLinkedDevice and return a client error otherwise
- Fix clients so secondary devices redirect linking to the primary device
- Correct test harnesses to use a primary Device instance
Example fix
// before
accounts.waitForNewLinkedDevice(accountUuid, authenticatedDevice, token, timeout); // may be secondary
// after
if (!authenticatedDevice.isPrimary()) {
throw new WebApplicationException(Response.status(403).build());
}
accounts.waitForNewLinkedDevice(accountUuid, authenticatedDevice, token, timeout); Defensive patterns
Strategy: validation
Validate before calling
// Ensure the authenticated device is primary before initiating a link
if (!authenticatedDevice.isPrimary()) {
throw new WebApplicationException(Response.status(403).build());
} Type guard
static boolean canInitiateDeviceLink(Device d) {
return d.isPrimary();
} Try / catch
try {
return accounts.waitForNewLinkedDevice(accountUuid, device, token, timeout);
} catch (IllegalArgumentException e) {
if (e.getMessage().equals("Only primary devices can link devices")) {
return CompletableFuture.failedFuture(new WebApplicationException(Response.status(403).build()));
}
throw e;
} Prevention
- Route link-device requests exclusively through primary-device sessions
- Check isPrimary() in the API auth layer
- Fix clients that let secondary devices start provisioning
- Test link flows with both primary and secondary auth contexts
When it happens
Trigger: Calling waitForNewLinkedDevice() with a linkingDevice whose isPrimary() is false — e.g. a secondary device initiating the link-device handshake.
Common situations: Client bugs where the link request is routed from the wrong device; duplicated provisioning code running on a linked device; reversed arguments when constructing the linking Device object in tests.
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
- Cannot remove primary device
- PNI identity key must not be provided if existing account…
- end of range must be after start of range
- Missing required device capability
- cannot use play billing for one-time donations
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/6a2745ff6d17453c.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java:1708
final List<String> keysToDelete = new ArrayList<>(2);
account.getPhoneNumberIdentifier()
.map(pni -> getAccountMapKey(pni.toString()))
.ifPresent(keysToDelete::add);
keysToDelete.add(getAccountEntityKey(account.getAccountIdentifier()));
ResilienceUtil.getGeneralRedisRetry(RETRY_NAME).executeRunnable(() ->
redisDeleteTimer.record(() ->
cacheCluster.useCluster(connection ->
connection.sync().del(keysToDelete.toArray(String[]::new)))));
}
public CompletableFuture<Optional<DeviceInfo>> waitForNewLinkedDevice(
final UUID accountIdentifier,
final Device linkingDevice,
final String linkDeviceTokenIdentifier,
final Duration timeout) {
if (!linkingDevice.isPrimary()) {
throw new IllegalArgumentException("Only primary devices can link devices");
}
// Unbeknownst to callers but beknownst to us, the "link device token identifier" is the base64/url-encoded SHA256
// hash of a device-linking token. Before we use the string anywhere, make sure it's the right "shape" for a hash.
if (Base64.getUrlDecoder().decode(linkDeviceTokenIdentifier).length != SHA256_HASH_LENGTH) {
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid token identifier"));
}
final Instant deadline = clock.instant().plus(timeout);
final CompletableFuture<Optional<DeviceInfo>> deviceAdded = waitForPubSubKey(waitForDeviceFuturesByTokenIdentifier,
linkDeviceTokenIdentifier, getLinkedDeviceKey(linkDeviceTokenIdentifier), timeout, this::handleDeviceAdded);
return deviceAdded.thenCompose(maybeDeviceInfo -> maybeDeviceInfo.map(deviceInfo -> {
// The device finished linking, we now want to make sure the primary client has fetched messages that could
// have come in before the linked device's mailbox was set up. This avoids a race where the linked device
// misses out on messages that were sent before its mailbox was set up but received by the primary *after*
// creating its backup for the linked device.
View on GitHub (pinned to 100ab61c82)