{"record":{"id":"69dab39556b6092b","repo":"signalapp/Signal-Server","slug":"cannot-remove-primary-device","errorCode":null,"errorMessage":"Cannot remove primary device","messagePattern":"Cannot remove primary device","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java","lineNumber":899,"sourceCode":"\n    final Instant tokenExpiration = timestamp.plus(LINK_DEVICE_TOKEN_EXPIRATION_DURATION);\n\n    if (tokenExpiration.isBefore(clock.instant())) {\n      return Optional.empty();\n    }\n\n    return Optional.of(aci);\n  }\n\n  /**\n   * Unlink a device from the given account. The device will be immediately disconnected if it is connected to any chat\n   * frontend.\n   *\n   * @return the updated Account\n   */\n  public Account removeDevice(final UUID accountIdentifier, final byte deviceId) {\n    if (deviceId == Device.PRIMARY_ID) {\n      throw new IllegalArgumentException(\"Cannot remove primary device\");\n    }\n\n    // Always fetch a fresh, non-cached copy of the account before making modifications\n    final Account account = accounts.getByAccountIdentifier(accountIdentifier)\n        .orElseThrow(() -> new IllegalArgumentException(\"Account not found: \" + accountIdentifier));\n\n    return accountLockManager.withSingleAccountLock(account,\n        () -> removeDevice(accountIdentifier, deviceId, MAX_UPDATE_ATTEMPTS));\n  }\n\n  private Account removeDevice(final UUID accountIdentifier, final byte deviceId, final int retries) {\n    final Account account = accounts.getByAccountIdentifier(accountIdentifier)\n        .orElseThrow(ContestedOptimisticLockException::new);\n\n    CompletableFuture.allOf(\n            keysManager.deleteSingleUsePreKeys(account.getAccountIdentifier(), deviceId),\n            account.getPhoneNumberIdentifier()\n                .map(pni -> keysManager.deleteSingleUsePreKeys(pni, deviceId))","sourceCodeStart":881,"sourceCodeEnd":917,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java#L881-L917","documentation":"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.","triggerScenarios":"Calling removeDevice(accountIdentifier, deviceId) with deviceId == Device.PRIMARY_ID (1).","commonSituations":"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.","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"],"exampleFix":"// before\naccounts.removeDevice(accountUuid, request.deviceId());\n// after\nif (request.deviceId() == Device.PRIMARY_ID) {\n  throw new WebApplicationException(Response.status(400).build());\n}\naccounts.removeDevice(accountUuid, request.deviceId());","handlingStrategy":"validation","validationCode":"// Guard before calling removeDevice\nif (deviceId == Device.PRIMARY_ID) {\n  throw new WebApplicationException(Response.status(400).build());\n}","typeGuard":"static boolean isRemovableDevice(byte deviceId) {\n  return deviceId != Device.PRIMARY_ID;\n}","tryCatchPattern":"try {\n  accounts.removeDevice(accountUuid, deviceId);\n} catch (IllegalArgumentException e) {\n  if (e.getMessage().equals(\"Cannot remove primary device\")) {\n    return Response.status(400).build();\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["illegal-argument","devices","unsupported-operation","accounts"],"backgroundTag":"unsupported-operation","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}