{"record":{"id":"4e84923ae95be752","repo":"signalapp/Signal-Server","slug":"must-set-at-least-one-of-message-media-credential","errorCode":null,"errorMessage":"Must set at least one of message/media credential requests","messagePattern":"Must set at least one of message/media credential requests","errorType":"exception","errorClass":"BackupInvalidArgumentException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java","lineNumber":111,"sourceCode":"   * @param device                          The device setting the account backup-id\n   * @param messagesBackupCredentialRequest A request containing the blinded backup-id the client will use to upload\n   *                                        message backups\n   * @param mediaBackupCredentialRequest    A request containing the blinded backup-id the client will use to upload\n   *                                        media backups\n   * @throws RateLimitExceededException If too many backup-ids have been committed\n   */\n  public void commitBackupId(\n      final Account account,\n      final Device device,\n      final Optional<BackupAuthCredentialRequest> messagesBackupCredentialRequest,\n      final Optional<BackupAuthCredentialRequest> mediaBackupCredentialRequest)\n      throws RateLimitExceededException, BackupPermissionException, BackupInvalidArgumentException {\n    if (!device.isPrimary()) {\n      throw new BackupPermissionException(\"Only primary device can set backup-id\");\n    }\n\n    if (messagesBackupCredentialRequest.isEmpty() && mediaBackupCredentialRequest.isEmpty()) {\n      throw new BackupInvalidArgumentException(\"Must set at least one of message/media credential requests\");\n    }\n\n    final byte[] storedMessageCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MESSAGES)\n        .orElse(null);\n    final byte[] storedMediaCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MEDIA)\n        .orElse(null);\n\n    // If the provided credential request is null, we want to set to the existing request\n    final byte[] targetMessageCredentialRequest = messagesBackupCredentialRequest\n        .map(BackupAuthCredentialRequest::serialize)\n        .orElse(storedMessageCredentialRequest);\n    final byte[] targetMediaCredentialRequest = mediaBackupCredentialRequest\n        .map(BackupAuthCredentialRequest::serialize)\n        .orElse(storedMediaCredentialRequest);\n\n    final boolean requiresMessageRotation =\n        !MessageDigest.isEqual(targetMessageCredentialRequest, storedMessageCredentialRequest);\n    final boolean requiresMediaRotation =","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java#L93-L129","documentation":"commitBackupId requires at least one BackupAuthCredentialRequest: the caller must request credentials for message backups, media backups, or both. Supplying two empty Optionals is a client contract violation surfaced as BackupInvalidArgumentException.","triggerScenarios":"Calling commitBackupId / PUT /v1/backups/ with a request body omitting both messagesBackupCredentialRequest and mediaBackupCredentialRequest.","commonSituations":"Older clients predating the media-credential field sending empty payloads; clients serializing nullable fields incorrectly so both Optionals end up empty; API consumers hand-building JSON bodies.","solutions":["Include at least one of the message or media BackupAuthCredentialRequest fields in the request body.","Fix client serialization so present credential requests are not dropped as null.","Validate the request payload before invoking the manager and return a 400 with a precise message."],"exampleFix":"// before\nbackupAuthManager.commitBackupId(account, device, Optional.empty(), Optional.empty());\n// after\nif (messagesReq.isEmpty() && mediaReq.isEmpty()) {\n  throw new BadRequestException(\"must set at least one of message/media credential requests\");\n}\nbackupAuthManager.commitBackupId(account, device, messagesReq, mediaReq);","handlingStrategy":"validation","validationCode":"if (messagesReq.isEmpty() && mediaReq.isEmpty()) {\n  throw new BadRequestException(\"at least one of message/media credential requests is required\");\n}","typeGuard":"boolean hasAtLeastOneCredentialRequest(Optional<BackupAuthCredentialRequest> m, Optional<BackupAuthCredentialRequest> med) {\n  return m.isPresent() || med.isPresent();\n}","tryCatchPattern":"try {\n  backupAuthManager.commitBackupId(account, device, messagesReq, mediaReq);\n} catch (BackupInvalidArgumentException e) {\n  return Response.status(400).entity(e.getMessage()).build();\n}","preventionTips":["Make the request schema require oneOf(messages, media) credential fields","Verify client serializers do not drop empty-but-present fields as null","Add contract tests covering both-only, either-only, and neither payloads"],"tags":["validation","backup","missing-required-field"],"backgroundTag":"missing-required-argument","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"}