{"record":{"id":"aab9d32a722df3ff","repo":"signalapp/Signal-Server","slug":"missing-required-device-capability","errorCode":null,"errorMessage":"Missing required device capability","messagePattern":"Missing required device capability","errorType":"http","errorClass":"WebApplicationException","httpStatus":409,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java","lineNumber":248,"sourceCode":"      description = \"If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed\"))\n  public LinkDeviceResponse linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) @NotNull BasicAuthorizationHeader authorizationHeader,\n      @HeaderParam(HttpHeaders.USER_AGENT) @Nullable String userAgent,\n      @NotNull @Valid LinkDeviceRequest linkDeviceRequest)\n      throws RateLimitExceededException, DeviceLimitExceededException {\n    final Account account = accounts.checkDeviceLinkingToken(linkDeviceRequest.verificationCode())\n        .flatMap(accounts::getByAccountIdentifier)\n        .orElseThrow(ForbiddenException::new);\n\n    final DeviceActivationRequest deviceActivationRequest = linkDeviceRequest.deviceActivationRequest();\n    final DeviceAttributes deviceAttributes = linkDeviceRequest.deviceAttributes();\n\n    rateLimiters.getVerifyDeviceLimiter().validate(account.getAccountIdentifier());\n\n    // Check the optional-phone-number capability before checking PNI keys, so we can give a better error code (since an\n    // older device will improperly supply PNI keys for a PNI-less account)\n    if (account.getPhoneNumberIdentifier().isEmpty() &&\n        !linkDeviceRequest.deviceAttributes().capabilities().contains(DeviceCapability.OPTIONAL_PHONE_NUMBER)) {\n      throw new WebApplicationException(\"Missing required device capability\", 409);\n    }\n\n    final boolean allKeysValid =\n      PreKeySignatureValidator\n          .validatePreKeySignatures(account.getAccountIdentityKey(),\n              List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey()),\n              userAgent,\n              \"link-device\")\n          && account.getPhoneNumberIdentityKey()\n                .map(pniIdentityKey ->\n                  deviceActivationRequest.pniSignedPreKey().isPresent()\n                      && deviceActivationRequest.pniPqLastResortPreKey().isPresent()\n                      && PreKeySignatureValidator.validatePreKeySignatures(\n                          pniIdentityKey,\n                          List.of(deviceActivationRequest.pniSignedPreKey().get(), deviceActivationRequest.pniPqLastResortPreKey().get()),\n                          userAgent,\n                          \"link-device\"))\n              .orElse(","sourceCodeStart":230,"sourceCodeEnd":266,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java#L230-L266","documentation":"DeviceController.linkDevice rejects linking a new device that does not advertise the OPTIONAL_PHONE_NUMBER capability when the account has no PhoneNumberIdentifier (PNI). The capability is required because the new device must support accounts without a PNI; without it the server returns 409 with this message before validating PNI keys.","triggerScenarios":"Calling the link-device (PUT /v1/devices/link) flow for an account whose getPhoneNumberIdentifier() is empty, while the submitted linkDeviceRequest's deviceAttributes.capabilities() does not contain DeviceCapability.OPTIONAL_PHONE_NUMBER.","commonSituations":"Older or third-party clients that predate PNI support linking to a newer account created without a PNI; stale client builds that omit the optional-phone-number capability flag; accounts migrated to PNI-less state.","solutions":["Update the linking client to declare DeviceCapability.OPTIONAL_PHONE_NUMBER in its capabilities set","Upgrade the client app to a version that supports phone-number identifiers","If you control the account state, ensure the account has a PNI or use a client matching the account's requirements","Verify the capabilities payload sent during device activation includes optional-phone-number"],"exampleFix":"// before\nSet<DeviceCapability> caps = EnumSet.of(DeviceCapability.STORAGE); // missing OPTIONAL_PHONE_NUMBER\nlinkDevice(account, new LinkDeviceRequest(attrs(caps), ...)); // 409 Missing required device capability\n// after\nSet<DeviceCapability> caps = EnumSet.of(DeviceCapability.STORAGE, DeviceCapability.OPTIONAL_PHONE_NUMBER);\nlinkDevice(account, new LinkDeviceRequest(attrs(caps), ...));","handlingStrategy":"validation","validationCode":"if (account.getPhoneNumberIdentifier().isEmpty()\n    && !deviceAttributes.capabilities().contains(DeviceCapability.OPTIONAL_PHONE_NUMBER)) {\n  // upgrade client or add capability before attempting to link\n}","typeGuard":null,"tryCatchPattern":"try {\n  client.linkDevice(account, request);\n} catch (WebApplicationException e) {\n  if (e.getResponse().getStatus() == 409) {\n    // add DeviceCapability.OPTIONAL_PHONE_NUMBER and retry\n  } else throw e;\n}","preventionTips":["Always advertise OPTIONAL_PHONE_NUMBER in new clients","Test linking against PNI-less accounts","Keep client capability flags current with server expectations"],"tags":["device-linking","capabilities","conflict-409"],"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"}