signalapp/Signal-Server · error · IllegalArgumentException

redemption window too large

Error message

redemption window too large

What it means

RedemptionRange.inclusive() enforces a maximum window width: end minus start must not exceed MAX_REDEMPTION_DURATION, regardless of where the range sits relative to today. The error indicates the requested span is too wide.

Solutions

  1. Shrink the requested window to at most MAX_REDEMPTION_DURATION and paginate/iterate over multiple ranges.
  2. Cap the end: redemptionEnd = min(requestedEnd, redemptionStart.plus(MAX_REDEMPTION_DURATION)).
  3. Read the current constant value from source to confirm the allowed span after upgrades.

Example fix

// before
RedemptionRange.inclusive(clock, start, start.plus(Duration.ofDays(30)));
// after
Instant end = start.plus(Duration.ofDays(30));
while (!start.isAfter(requestedEnd)) {
  RedemptionRange.inclusive(clock, start, end.minusSeconds(1));
  start = end;
  end = start.plus(MAX_REDEMPTION_DURATION);
}
Defensive patterns

Strategy: validation

Validate before calling

if (end.minusSeconds(0).isAfter(start.plus(MAX_REDEMPTION_DURATION))) {
  end = start.plus(MAX_REDEMPTION_DURATION);
}

Type guard

boolean windowFits(Instant start, Instant end) {
  return !end.isAfter(start.plus(MAX_REDEMPTION_DURATION));
}

Try / catch

try {
  RedemptionRange.inclusive(clock, start, end);
} catch (IllegalArgumentException e) {
  if (e.getMessage().contains("window too large")) {
    iterateWindows(start, end, MAX_REDEMPTION_DURATION);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Calling inclusive() with redemptionEnd > redemptionStart + MAX_REDEMPTION_DURATION, e.g. a 60-day window when the maximum is a few days.

Common situations: Export/reporting jobs requesting long history at once; clients multiplying days incorrectly; policy change reducing MAX_REDEMPTION_DURATION while callers still request old spans.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/auth/RedemptionRange.java:72

    if (redemptionStart.isAfter(redemptionEnd)) {
      throw new IllegalArgumentException("end of range must be after start of range");
    }

    if (!redemptionStart.truncatedTo(ChronoUnit.DAYS).equals(redemptionStart)
        || !redemptionEnd.truncatedTo(ChronoUnit.DAYS).equals(redemptionEnd)) {
      throw new IllegalArgumentException("timestamps must be day aligned");
    }

    if (redemptionStart.isBefore(yesterday)) {
      throw new IllegalArgumentException("start of range too far in the past");
    }

    if (redemptionEnd.isAfter(today.plus(MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1)))) {
      throw new IllegalArgumentException("end of range too far in the future");
    }

    if (redemptionEnd.isAfter(redemptionStart.plus(MAX_REDEMPTION_DURATION))) {
      throw new IllegalArgumentException("redemption window too large");
    }

    return new RedemptionRange(
        LocalDate.ofInstant(redemptionStart, ZoneOffset.UTC),
        LocalDate.ofInstant(redemptionEnd, ZoneOffset.UTC));
  }

  @Override
  public @NotNull Iterator<Instant> iterator() {
    final Instant fromInstant = from.atStartOfDay(ZoneOffset.UTC).toInstant();
    final Instant endInstant = end.atStartOfDay(ZoneOffset.UTC).toInstant();
    return Stream
        .iterate(fromInstant, redemptionTime -> redemptionTime.plus(Duration.ofDays(1)))
        .takeWhile(redemptionTime -> !redemptionTime.isAfter(endInstant))
        .iterator();
  }

  @Override

View on GitHub (pinned to 100ab61c82)