{"record":{"id":"ba7e3160a1aa63d7","repo":"signalapp/Signal-Server","slug":"group-send-endorsement-tokens-should-not-be-sent-f","errorCode":null,"errorMessage":"Group send endorsement tokens should not be sent for story messages","messagePattern":"Group send endorsement tokens should not be sent for story messages","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java","lineNumber":223,"sourceCode":"\n      @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,\n\n      @Parameter(description=\"The recipient’s account or phone-number identifier\")\n      @PathParam(\"destination\") final ServiceIdentifier destinationIdentifier,\n\n      @Parameter(description=\"If true, the message is a story; access tokens are not checked and sending to nonexistent recipients is permitted\")\n      @QueryParam(\"story\") final boolean isStory,\n\n      @Parameter(description=\"The encrypted message payloads for each recipient device\")\n      @NotNull @Valid final IncomingMessageList messages,\n\n      @Context final ContainerRequestContext context) throws RateLimitExceededException {\n\n    if (groupSendToken != null) {\n      if (source.isPresent() || accessKey.isPresent()) {\n        throw new BadRequestException(\"Group send endorsement tokens should not be combined with other authentication\");\n      } else if (isStory) {\n        throw new BadRequestException(\"Group send endorsement tokens should not be sent for story messages\");\n      }\n    }\n\n    final Sample sample = Timer.start();\n    final boolean needsSync;\n\n    try {\n      if (isStory) {\n        needsSync = false;\n        sendStoryMessage(destinationIdentifier, messages, context);\n      } else if (source.isPresent()) {\n        final AuthenticatedDevice authenticatedDevice = source.get();\n        final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())\n            .orElseThrow(() -> new WebApplicationException(Status.UNAUTHORIZED));\n\n        if (account.isIdentifiedBy(destinationIdentifier)) {\n          needsSync = false;\n          sendSyncMessage(source.get(), account, destinationIdentifier, messages, context);","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java#L205-L241","documentation":"Story messages must be sent without any sender authentication, so providing a group send endorsement token with a story send is rejected with a 400. Stories use a distinct unauthenticated path, and endorsement tokens are meaningless for them.","triggerScenarios":"POST /v1/messages with isStory=true (or the story flag in the payload) while the group send endorsement token header is present.","commonSituations":"Client code that attaches endorsement tokens unconditionally to all sends; story flag added after endorsement logic; shared request-builder code reused for stories and normal messages.","solutions":["Strip the group send endorsement token header when sending story messages.","Branch client send logic: stories use the no-auth story path; normal messages may use the token.","If the token was intended for a normal message, unset the story flag."],"exampleFix":"// before\nrequest.header(\"X-Group-Send-Token\", token); payload.story = true; // conflict\n// after\nif (payload.story) { /* no token header */ } else { request.header(\"X-Group-Send-Token\", token); }","handlingStrategy":"validation","validationCode":"if (message.isStory && headers[\"X-Group-Send-Token\"]) { delete headers[\"X-Group-Send-Token\"]; }","typeGuard":null,"tryCatchPattern":"try { await sendStory(); } catch (e) { if (e.status === 400 && /story messages/.test(e.body)) { resendWithoutGroupSendToken(); } }","preventionTips":["Branch send logic on message type before setting headers","Never reuse a request builder configured for normal messages for stories","Add a unit test asserting story requests carry no endorsement token"],"tags":["http-400","stories","group-send-endorsements"],"backgroundTag":"mutually-exclusive-options","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"}