signalapp/Signal-Server · error · WebApplicationException

.status(410)

Error message

.status(410)

What it means

sendMultiRecipientMessage returns HTTP 410 Gone when one or more recipient accounts report stale devices: the sender encrypted for device IDs the server has retired. The body lists AccountStaleDevices with each account's staleDeviceIds so the sender can discard sessions for those devices and resend without them.

Solutions

  1. Discard local sessions for the staleDeviceIds in the 410 body and rebuild sessions from fresh prekeys
  2. Re-encrypt and resend the message excluding stale devices
  3. Clear cached recipient key material and refetch prekeys for accounts named in the response
  4. Treat repeated 410s as a signal to resync the group's device lists

Example fix

// before
sendMessage(multiRecipientPayload);
// after
try { sendMessage(multiRecipientPayload); }
catch (StaleDevices410 e) {
  for (AccountStaleDevices a : parseStale(e.body())) {
    a.staleDevices().forEach(id -> sessionStore.deleteSession(a.accountId(), id));
  }
  resend();
}
Defensive patterns

Strategy: fallback

Validate before calling

// evict known-retired device ids from the send set before encrypting
recipients.forEach(r -> sendSet.removeAll(retiredDeviceCache.get(r.accountId())));

Try / catch

if (response.code() == 410) {
  parseStaleDevices(response.body()).forEach(a ->
      a.staleDeviceIds().forEach(id -> sessionStore.delete(a.accountId(), id)));
  reencryptAndResend();
}

Prevention

When it happens

Trigger: POSTing a multi-recipient message containing ciphertext for device IDs that were since deactivated (device unlinked, account re-registered, device retired).

Common situations: Recipients re-registering (which invalidates all old device IDs) or unlinking tablets/desktops; senders with cached session state from before a recipient's device rotation; long-offline clients catching up on group messages.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


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

Appendix: source

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

              .toList();

      if (!accountMismatchedDevices.isEmpty()) {
        throw new WebApplicationException(Response
            .status(409)
            .type(MediaType.APPLICATION_JSON_TYPE)
            .entity(accountMismatchedDevices)
            .build());
      }

      final List<AccountStaleDevices> accountStaleDevices =
          e.getMismatchedDevicesByServiceIdentifier().entrySet().stream()
              .filter(entry -> !entry.getValue().staleDeviceIds().isEmpty())
              .map(entry -> new AccountStaleDevices(entry.getKey(),
                  new StaleDevicesResponse(entry.getValue().staleDeviceIds())))
              .toList();

      throw new WebApplicationException(Response
          .status(410)
          .type(MediaType.APPLICATION_JSON)
          .entity(accountStaleDevices)
          .build());
    } catch (final MessageDeliveryNotAllowedException e) {
      throw new ServiceUnavailableException();
    }
  }

  private void checkGroupSendToken(final Collection<ServiceId> recipients, final GroupSendTokenHeader groupSendToken) {
    checkGroupSendToken(recipients, groupSendToken.token());
  }

  private void checkGroupSendToken(final Collection<ServiceId> recipients, final GroupSendFullToken groupSendFullToken) {
    try {
      groupSendFullToken.verify(recipients,
          clock.instant(),
          GroupSendDerivedKeyPair.forExpiration(groupSendFullToken.getExpiration(), serverSecretParams));
    } catch (final VerificationFailedException e) {

View on GitHub (pinned to 100ab61c82)