signalapp/Signal-Server · error · IllegalArgumentException
PNI identity key must not be provided if existing account…
Error message
PNI identity key must not be provided if existing account does not have a phone number
What it means
During account creation/update, AccountsManager throws IllegalArgumentException("PNI identity key must not be provided if existing account does not have a phone number") when a caller supplies a PNI identity key for an account that has no phone number (and thus no PNI). The PNI identity key only makes sense for accounts with a phone-number identity.
Solutions
- Only include pniIdentityKey in the request when the target account has a phone number (account.getPhoneNumberIdentifier().isPresent())
- Check existingAccount.getPhoneNumberIdentifier().isPresent() before passing the key
- Update older clients/tools to omit the PNI key for non-PNI accounts
- Fix test fixtures to match the account shape
Example fix
// before
manager.changeNumber(account, number, pniIdentityKey, pniSigningKey, attributes); // always passes key
// after
final Optional<IdentityKey> effectivePniKey = account.getPhoneNumberIdentifier().isPresent()
? Optional.of(pniIdentityKey) : Optional.empty();
manager.changeNumber(account, number, effectivePniKey, pniSigningKey, attributes); Defensive patterns
Strategy: validation
Validate before calling
// Only pass PNI identity key when the account has a PNI
final boolean hasPni = existingAccount.getPhoneNumberIdentifier().isPresent();
if (!hasPni && pniIdentityKey.isPresent()) {
throw new WebApplicationException(Response.status(400).build());
} Type guard
static boolean canAcceptPniIdentityKey(Account account, Optional<IdentityKey> pniKey) {
return pniKey.isEmpty() || account.getPhoneNumberIdentifier().isPresent();
} Try / catch
try {
accounts.changeNumber(...);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("PNI identity key must not be provided")) {
return Response.status(400).entity(e.getMessage()).build();
}
throw e;
} Prevention
- Always check getPhoneNumberIdentifier().isPresent() before supplying a PNI key
- Keep legacy account flows free of PNI parameters
- Mirror this validation in the API layer for early 400s
- Review fixtures/tests for mixed account shapes
When it happens
Trigger: Calling changeNumber or account creation flows passing a pniIdentityKey Optional.of(...) while the existing account lacks a phone number.
Common situations: Client migration code written before PNI existed being applied to legacy/no-PNI accounts; copy-paste in account-update endpoints passing all keys unconditionally; tests using fixtures mixing old and new account shapes.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- end of range must be after start of range
- Cannot remove primary device
- Only primary devices can link devices
- Blank header
- timestamps must be day aligned
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/11ca5cc6da435711.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java:593
@Nullable final String userAgent) {
final Account recoveredAccount = accountLockManager.withSingleAccountLock(existingAccount, () -> {
final Account account = new Account();
account.setAccountIdentifier(existingAccount.getAccountIdentifier());
if (existingAccount.getNumber().isPresent()) {
account.setNumber(existingAccount.getNumber().get(),
existingAccount.getPhoneNumberIdentifier()
.orElseThrow(() -> new AssertionError("Accounts that have a phone number must also have a PNI")));
account.setPhoneNumberIdentityKey(maybePniIdentityKey
.orElseThrow(() -> new IllegalArgumentException("PNI identity key must be provided if existing account has a phone number")));
account.setRegistrationLockFromAttributes(accountAttributes);
account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
} else {
if (maybePniIdentityKey.isPresent()) {
throw new IllegalArgumentException("PNI identity key must not be provided if existing account does not have a phone number");
}
final byte[] authCredentialSalt = new byte[AUTH_CREDENTIAL_SALT_SIZE];
SECURE_RANDOM.nextBytes(authCredentialSalt);
account.setAuthCredentialSalt(authCredentialSalt);
}
account.setIdentityKey(aciIdentityKey);
account.addDevice(primaryDeviceSpec.toDevice(Device.PRIMARY_ID, clock, aciIdentityKey));
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
account.setAccountRecoveryPassword(accountAttributes.recoveryPassword().orElseThrow(() ->
new IllegalArgumentException("Must specify a recovery password when reclaiming an existing account")));
reclaimAccount(account, existingAccount, primaryDeviceSpec, accountAttributes);
return account;View on GitHub (pinned to 100ab61c82)