signalapp/Signal-Server · error · InternalServerErrorException
interrupted during delivery
Error message
interrupted during delivery
What it means
Thrown as HTTP 500 when the executor thread delivering multi-recipient messages is interrupted while the server waits on the futures of per-recipient deliveries. The server logs the interruption and converts it into an InternalServerErrorException.
Solutions
- Retry the send; it is a transient server-side interruption
- If the client caused the timeout, increase the client HTTP timeout for multi-recipient sends
- Check server logs for shutdown/interruption cause and reduce batch size if frequently hit
Defensive patterns
Strategy: retry
Try / catch
try { await sendMultiRecipientMessage(msg); } catch (e) { if (e.status === 500 && /interrupted/.test(e.message)) await withBackoff(() => send(msg)); else throw e; } Prevention
- Retry idempotent multi-recipient sends with backoff
- Set generous client timeouts for large fan-out sends
- Avoid triggering sends right at server maintenance windows
When it happens
Trigger: Thread interruption (e.g. server shutdown, request timeout canceling the worker) while sendMultiRecipientMessage blocks on Future.get() for the batch of recipient deliveries.
Common situations: Graceful shutdown or load-balancer timeout cutting off a large multi-recipient story send; executor shutdown mid-flight.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
Related errors
- delivery cancelled
- failure during delivery
- Only primary devices may register attestations
- Invalid action:
- Multi-recipient messages must be addressed to ACI service…
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/e691a99f199bc23a.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java:652
final MessageType messageType =
isStory ? MessageType.MULTI_RECIPIENT_STORY : MessageType.MULTI_RECIPIENT_SEALED_SENDER;
spamChecker.checkForMultiRecipientSpamHttp(messageType, context).response().ifPresent(response -> {
throw new WebApplicationException(response);
});
try {
if (!resolvedRecipients.isEmpty()) {
messageSender.sendMultiRecipientMessage(multiRecipientMessage,
resolvedRecipients,
timestamp, isStory,
ephemeral,
urgent,
context.getHeaderString(HttpHeaders.USER_AGENT)).get();
}
} catch (final InterruptedException e) {
logger.error("interrupted while delivering multi-recipient messages", e);
throw new InternalServerErrorException("interrupted during delivery");
} catch (final CancellationException e) {
logger.error("cancelled while delivering multi-recipient messages", e);
throw new InternalServerErrorException("delivery cancelled");
} catch (final ExecutionException e) {
logger.error("partial failure while delivering multi-recipient messages", e.getCause());
throw new InternalServerErrorException("failure during delivery");
} catch (final MessageTooLargeException e) {
throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE);
} catch (final MultiRecipientMismatchedDevicesException e) {
final List<AccountMismatchedDevices> accountMismatchedDevices =
e.getMismatchedDevicesByServiceIdentifier().entrySet().stream()
.filter(entry -> !entry.getValue().missingDeviceIds().isEmpty() || !entry.getValue().extraDeviceIds().isEmpty())
.map(entry -> new AccountMismatchedDevices(entry.getKey(),
new MismatchedDevicesResponse(entry.getValue().missingDeviceIds(), entry.getValue().extraDeviceIds())))
.toList();
if (!accountMismatchedDevices.isEmpty()) {
throw new WebApplicationException(ResponseView on GitHub (pinned to 100ab61c82)