signalapp/Signal-Server · error · BadRequestException

Multi-recipient message contains duplicate recipient

Error message

Multi-recipient message contains duplicate recipient

What it means

The server performs a cheap duplicate-device check on the multi-recipient message before resolving any accounts; if the serialized payload lists the same (serviceId, deviceId) pair more than once, the request is rejected with a 400. Each recipient device may appear exactly once.

Solutions

  1. Deduplicate recipients by (serviceId, deviceId) pair before serializing the multi-recipient message.
  2. If merging multiple recipient sources, union them instead of concatenating.
  3. Fix the per-device iteration so each device contributes exactly one entry.

Example fix

// before
recipients.addAll(groupMembers);
recipients.addAll(pinnedChats); // may contain duplicates
// after
Set<SignalServiceAddress> unique = new LinkedHashSet<>();
unique.addAll(groupMembers); unique.addAll(pinnedChats);
buildMultiRecipientMessage(new ArrayList<>(unique));
Defensive patterns

Strategy: validation

Validate before calling

const seen = new Set();
for (const r of recipients) { const key = `${r.serviceId}:${r.deviceId}`; if (seen.has(key)) throw new Error('duplicate recipient'); seen.add(key); }

Type guard

function hasNoDuplicates(recipients) { const keys = recipients.map(r => `${r.serviceId}:${r.deviceId}`); return new Set(keys).size === keys.length; }

Try / catch

try { await sendMultiRecipient(msg); } catch (e) { if (e.status === 400 && /duplicate recipient/.test(e.body)) { dedupeAndRebuildMessage(); } }

Prevention

When it happens

Trigger: POST to the multi-recipient endpoint where MessageUtil.hasDuplicateDevices detects a repeated recipient device entry in the payload.

Common situations: Client built the recipient list by merging overlapping sources (group members + distribution list) without deduplicating; retry logic appended recipients twice; bug in per-device key iteration.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java:535

      }

      return Response.ok(sendMultiRecipientMessageResponse).build();
    } finally {
      sample.stop(MULTI_RECIPIENT_MESSAGE_LATENCY_TIMER);
    }
  }

  private SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SealedSenderMultiRecipientMessage multiRecipientMessage,
      final long timestamp,
      final boolean ephemeral,
      final boolean urgent,
      @Nullable final GroupSendTokenHeader groupSendTokenHeader,
      @Nullable final CombinedUnidentifiedSenderAccessKeys combinedUnidentifiedSenderAccessKeys,
      final ContainerRequestContext context) {

    // Perform fast, inexpensive checks before attempting to resolve recipients
    if (MessageUtil.hasDuplicateDevices(multiRecipientMessage)) {
      throw new BadRequestException("Multi-recipient message contains duplicate recipient");
    }

    if (groupSendTokenHeader == null && combinedUnidentifiedSenderAccessKeys == null) {
      throw new NotAuthorizedException("A group send endorsement token or unidentified access key is required for non-story messages");
    }

    if (groupSendTokenHeader != null && combinedUnidentifiedSenderAccessKeys != null) {
      throw new BadRequestException("Only one of group send endorsement token and unidentified access key may be provided");
    }

    if (groupSendTokenHeader != null) {
      // Group send endorsements are checked before we even attempt to resolve any accounts, since
      // the lists of service IDs in the envelope are all that we need to check against
      checkGroupSendToken(multiRecipientMessage.getRecipients().keySet(), groupSendTokenHeader);
    } else {
      Metrics.counter(LEGACY_COMBINED_UAK_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(context))).increment();
    }

View on GitHub (pinned to 100ab61c82)