signalapp/Signal-Server · warning · BadRequestException

account does not have a phone number

Error message

account does not have a phone number

What it means

In setAccountAttributes, if the request attempts to set a registration lock (RegistrationLock) on an account that has no phone number (e.g. a username-only or tokenized account), the server cannot associate the registration lock, so it rejects with BadRequestException('account does not have a phone number').

Solutions

  1. Remove the registrationLock field from the attributes request for accounts without a phone number.
  2. Set up a phone number for the account before configuring a registration lock.
  3. Update client logic to hide/disable registration-lock setup when the account has no phone number.
  4. If server-side, gate the feature so number-less accounts skip registration lock handling.

Example fix

// before
attributes.setRegistrationLock(registrationLock); // account has no number
// after
if (account.getNumber().isPresent()) {
  attributes.setRegistrationLock(registrationLock);
}
Defensive patterns

Strategy: type-guard

Validate before calling

const canSetRegistrationLock = (acct) => acct.number != null && acct.number !== '';

Type guard

const hasPhoneNumber = (a) => typeof a.number === 'string' && a.number.length > 0;

Try / catch

try { await setAttributes(attrs); } catch (e) { if (e.status === 400 && e.message.includes('does not have a phone number')) { delete attrs.registrationLock; return setAttributes(attrs); } throw e; }

Prevention

When it happens

Trigger: PUT /v1/accounts/attributes with attributes containing a non-blank registrationLock while the authenticated account's number (a.getNumber()) is empty.

Common situations: Newer clients enabling registration lock on accounts created without a phone number, or after account migration to number-less identities where registration lock is unsupported.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/965c722c6149b1ee. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java:243

    final Collection<TransactWriteItem> additionalWriteItems =
        account.getPhoneNumberIdentifier()
            .flatMap(phoneNumberIdentifier -> attributes.recoveryPassword().map(recoveryPassword ->
                List.of(phoneNumberRecoveryPasswordsManager.buildTransactWriteItemForStorePassword(phoneNumberIdentifier, recoveryPassword))))
            .orElseGet(Collections::emptyList);

    accounts.update(auth.accountIdentifier(), a -> {
      a.getDevice(auth.deviceId()).ifPresent(d -> {
        d.setFetchesMessages(attributes.getFetchesMessages());
        d.setName(attributes.getName());
        d.setLastSeen(Util.todayInMillis());
        d.setCapabilities(attributes.getCapabilities());
        if (StringUtils.isNotBlank(signalAgent)) {
          d.setUserAgent(signalAgent);
        }
      });

      if (StringUtils.isNotEmpty(attributes.getRegistrationLock()) && a.getNumber().isEmpty()) {
        throw new BadRequestException("account does not have a phone number");
      }

      a.setRegistrationLockFromAttributes(attributes);
      a.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
      a.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());

      if (attributes.isDiscoverableByPhoneNumber() && a.getNumber().isEmpty()) {
        throw new BadRequestException("account does not have a phone number");
      }

      a.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber());

      attributes.recoveryPassword().ifPresent(a::setAccountRecoveryPassword);
    }, additionalWriteItems);
  }

  @GET
  @Path("/whoami")

View on GitHub (pinned to 100ab61c82)