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
- 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.
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
- 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
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
- Group send token not allowed when sending stories
- Group send endorsement tokens should not be combined with…
- Only one of group send endorsement token and unidentified…
- Invalid length
- Invalid create call link credential request
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)