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
- Discard local sessions for the staleDeviceIds in the 410 body and rebuild sessions from fresh prekeys
- Re-encrypt and resend the message excluding stale devices
- Clear cached recipient key material and refetch prekeys for accounts named in the response
- 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
- Delete sessions for retired device IDs as soon as a 410 lists them
- Refetch prekeys after any recipient re-registration
- Don't cache recipient device lists across long offline periods
- Distinguish 409 (missing+extra) from 410 (stale) handling
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
- .status(409)
- Empty body not allowed
- Got a non-200 reply from source URI:
- return Response.status(428).build();
- return Response.status(429).build();
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)