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
- Deduplicate recipients by (serviceId, deviceId) pair before serializing the multi-recipient message.
- If merging multiple recipient sources, union them instead of concatenating.
- 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
- Deduplicate by (serviceId, deviceId) before serialization
- Use a Set when merging recipient lists from multiple sources
- Verify per-device key material is generated exactly once per device
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
- Invalid length
- Invalid create call link credential request
- Illegal timestamp
- Recipient list is empty
- 400 Bad Request (UNSUPPORTED_LEVEL)
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)