signalapp/Signal-Server · error · InternalServerErrorException

delivery cancelled

Error message

delivery cancelled

What it means

Thrown as HTTP 500 when one of the futures delivering a multi-recipient message is cancelled before completion. The server logs the cancellation and returns InternalServerErrorException with 'delivery cancelled'.

Solutions

  1. Retry the send; cancellation is usually server-side and transient
  2. Increase client timeout to reduce the chance of server-side cancellation
  3. Inspect server logs to find which timeout/cancellation policy triggered it
Defensive patterns

Strategy: retry

Try / catch

try { await send(msg); } catch (e) { if (e.status === 500 && e.message === 'delivery cancelled') await withBackoff(() => send(msg)); else throw e; }

Prevention

When it happens

Trigger: Future.cancel() invoked on the per-recipient delivery futures, typically by a timeout or shutdown racing the delivery loop in sendMultiRecipientMessage.

Common situations: Server request timeout cancelling outstanding deliveries; executor shutdown during a large fan-out send.

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


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

Appendix: source

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

    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(Response
            .status(409)
            .type(MediaType.APPLICATION_JSON_TYPE)
            .entity(accountMismatchedDevices)

View on GitHub (pinned to 100ab61c82)