signalapp/Signal-Server · error · BadRequestException
Recovery password required for for storage when recovering…
Error message
Recovery password required for for storage when recovering an account by identifier
What it means
recoverAccount separately requires that the request's accountAttributes carry a recoveryPassword for storage on the account. When accountAttributes().recoveryPassword() is empty, the controller throws a BadRequestException (HTTP 400). Note the message contains a typo ('required for for storage'); it is distinct from the authentication password check at line 404.
Solutions
- Set accountAttributes.recoveryPassword (the storage copy) in addition to the top-level authentication recoveryPassword — both are required.
- Verify JSON serialization includes the recoveryPassword field inside accountAttributes (not just at the request root).
- Align the client's AccountAttributes builder with the current server API version so the field is populated.
- Update integration tests to assert both recovery-password fields are non-empty before calling the recovery endpoint.
Example fix
// before AccountAttributes attrs = new AccountAttributes(capabilities, true, null, name, pni, apnId); // after AccountAttributes attrs = new AccountAttributes(capabilities, true, storageRecoveryPassword, name, pni, apnId);
Defensive patterns
Strategy: validation
Validate before calling
if (request.accountAttributes().recoveryPassword().isEmpty()) {
throw new IllegalArgumentException("accountAttributes.recoveryPassword (storage) is required");
} Type guard
boolean hasStorageRecoveryPassword(RegistrationRequest r) {
return !r.accountAttributes().recoveryPassword().isEmpty();
} Try / catch
try {
recoverAccount(request);
} catch (BadRequestException e) {
if (e.getMessage().startsWith("Recovery password required for")) {
rebuildAttributesWithStorageRecoveryPassword();
} else throw e;
} Prevention
- Remember both recovery-password fields are required: request-level (auth) and accountAttributes-level (storage).
- Verify JSON serialization emits recoveryPassword inside accountAttributes.
- Add client-side tests covering both fields.
When it happens
Trigger: Submitting a recovery-by-identifier request where the top-level recoveryPassword is present but registrationRequest.accountAttributes().recoveryPassword() is empty — i.e. the attributes payload omits the storage recovery password.
Common situations: Client builds AccountAttributes without the recovery password (older schema or forgotten field); the two recovery-password fields are confused and only one is populated; request-mapping code drops the attribute during JSON deserialization.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- Recovery password required for authentication when…
- Must specify a PNI-associated identity key when recovering…
- account does not have a phone number
- Operation requires unauthenticated access
- must not use authenticated connection for anonymous…
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/550e8d407b5e1d4d.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java:408
}
}
private AccountCreationResponse recoverAccount(final UUID accountIdentifier,
final String password,
final RegistrationRequest registrationRequest,
final String userAgent,
final String signalAgent) throws RegistrationLockFailureException, RateLimitExceededException {
if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {
throw new BadRequestException("login purchases are not enabled");
}
if (ArrayUtils.isEmpty(registrationRequest.recoveryPassword())) {
throw new BadRequestException("Recovery password required for authentication when recovering an account by identifier");
}
if (registrationRequest.accountAttributes().recoveryPassword().isEmpty()) {
throw new BadRequestException("Recovery password required for for storage when recovering an account by identifier");
}
if (registrationRequest.pniIdentityKey() == null) {
throw new BadRequestException("Must specify a PNI-associated identity key when recovering an account by identifier");
}
final Account existingAccount = accounts.getByAccountIdentifier(accountIdentifier)
.orElseThrow(ForbiddenException::new);
final boolean passwordVerified = existingAccount.getAccountRecoveryPassword()
.map(saltedRecoveryPasswordHash -> PhoneNumberRecoveryPasswordsManager.verify(saltedRecoveryPasswordHash, registrationRequest.recoveryPassword()))
.orElse(false);
if (!passwordVerified) {
throw new ForbiddenException();
}
checkTotp(existingAccount, registrationRequest.totp());View on GitHub (pinned to 100ab61c82)