{"record":{"id":"d58ed8615439c0b2","repo":"signalapp/Signal-Server","slug":"multi-recipient-message-contains-duplicate-recipie","errorCode":null,"errorMessage":"Multi-recipient message contains duplicate recipient","messagePattern":"Multi-recipient message contains duplicate recipient","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java","lineNumber":535,"sourceCode":"      }\n\n      return Response.ok(sendMultiRecipientMessageResponse).build();\n    } finally {\n      sample.stop(MULTI_RECIPIENT_MESSAGE_LATENCY_TIMER);\n    }\n  }\n\n  private SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SealedSenderMultiRecipientMessage multiRecipientMessage,\n      final long timestamp,\n      final boolean ephemeral,\n      final boolean urgent,\n      @Nullable final GroupSendTokenHeader groupSendTokenHeader,\n      @Nullable final CombinedUnidentifiedSenderAccessKeys combinedUnidentifiedSenderAccessKeys,\n      final ContainerRequestContext context) {\n\n    // Perform fast, inexpensive checks before attempting to resolve recipients\n    if (MessageUtil.hasDuplicateDevices(multiRecipientMessage)) {\n      throw new BadRequestException(\"Multi-recipient message contains duplicate recipient\");\n    }\n\n    if (groupSendTokenHeader == null && combinedUnidentifiedSenderAccessKeys == null) {\n      throw new NotAuthorizedException(\"A group send endorsement token or unidentified access key is required for non-story messages\");\n    }\n\n    if (groupSendTokenHeader != null && combinedUnidentifiedSenderAccessKeys != null) {\n      throw new BadRequestException(\"Only one of group send endorsement token and unidentified access key may be provided\");\n    }\n\n    if (groupSendTokenHeader != null) {\n      // Group send endorsements are checked before we even attempt to resolve any accounts, since\n      // the lists of service IDs in the envelope are all that we need to check against\n      checkGroupSendToken(multiRecipientMessage.getRecipients().keySet(), groupSendTokenHeader);\n    } else {\n      Metrics.counter(LEGACY_COMBINED_UAK_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(context))).increment();\n    }\n","sourceCodeStart":517,"sourceCodeEnd":553,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java#L517-L553","documentation":"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.","triggerScenarios":"POST to the multi-recipient endpoint where MessageUtil.hasDuplicateDevices detects a repeated recipient device entry in the payload.","commonSituations":"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.","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."],"exampleFix":"// before\nrecipients.addAll(groupMembers);\nrecipients.addAll(pinnedChats); // may contain duplicates\n// after\nSet<SignalServiceAddress> unique = new LinkedHashSet<>();\nunique.addAll(groupMembers); unique.addAll(pinnedChats);\nbuildMultiRecipientMessage(new ArrayList<>(unique));","handlingStrategy":"validation","validationCode":"const seen = new Set();\nfor (const r of recipients) { const key = `${r.serviceId}:${r.deviceId}`; if (seen.has(key)) throw new Error('duplicate recipient'); seen.add(key); }","typeGuard":"function hasNoDuplicates(recipients) { const keys = recipients.map(r => `${r.serviceId}:${r.deviceId}`); return new Set(keys).size === keys.length; }","tryCatchPattern":"try { await sendMultiRecipient(msg); } catch (e) { if (e.status === 400 && /duplicate recipient/.test(e.body)) { dedupeAndRebuildMessage(); } }","preventionTips":["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"],"tags":["http-400","duplicates","validation"],"backgroundTag":"duplicate-entry","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"}