signalapp/Signal-Server · error · BadRequestException

Group send endorsement tokens should not be sent for story…

Error message

Group send endorsement tokens should not be sent for story messages

What it means

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.

Solutions

  1. Strip the group send endorsement token header when sending story messages.
  2. Branch client send logic: stories use the no-auth story path; normal messages may use the token.
  3. If the token was intended for a normal message, unset the story flag.

Example fix

// before
request.header("X-Group-Send-Token", token); payload.story = true; // conflict
// after
if (payload.story) { /* no token header */ } else { request.header("X-Group-Send-Token", token); }
Defensive patterns

Strategy: validation

Validate before calling

if (message.isStory && headers["X-Group-Send-Token"]) { delete headers["X-Group-Send-Token"]; }

Try / catch

try { await sendStory(); } catch (e) { if (e.status === 400 && /story messages/.test(e.body)) { resendWithoutGroupSendToken(); } }

Prevention

When it happens

Trigger: POST /v1/messages with isStory=true (or the story flag in the payload) while the group send endorsement token header is present.

Common situations: 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.

Related errors


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

Appendix: source

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

      @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,

      @Parameter(description="The recipient’s account or phone-number identifier")
      @PathParam("destination") final ServiceIdentifier destinationIdentifier,

      @Parameter(description="If true, the message is a story; access tokens are not checked and sending to nonexistent recipients is permitted")
      @QueryParam("story") final boolean isStory,

      @Parameter(description="The encrypted message payloads for each recipient device")
      @NotNull @Valid final IncomingMessageList messages,

      @Context final ContainerRequestContext context) throws RateLimitExceededException {

    if (groupSendToken != null) {
      if (source.isPresent() || accessKey.isPresent()) {
        throw new BadRequestException("Group send endorsement tokens should not be combined with other authentication");
      } else if (isStory) {
        throw new BadRequestException("Group send endorsement tokens should not be sent for story messages");
      }
    }

    final Sample sample = Timer.start();
    final boolean needsSync;

    try {
      if (isStory) {
        needsSync = false;
        sendStoryMessage(destinationIdentifier, messages, context);
      } else if (source.isPresent()) {
        final AuthenticatedDevice authenticatedDevice = source.get();
        final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())
            .orElseThrow(() -> new WebApplicationException(Status.UNAUTHORIZED));

        if (account.isIdentifiedBy(destinationIdentifier)) {
          needsSync = false;
          sendSyncMessage(source.get(), account, destinationIdentifier, messages, context);

View on GitHub (pinned to 100ab61c82)