signalapp/Signal-Server · error · IllegalArgumentException

end of range too far in the future

Error message

end of range too far in the future

What it means

RedemptionRange.inclusive() rejects ranges whose end extends beyond today plus MAX_REDEMPTION_DURATION plus one day. This prevents callers from requesting redemption windows reaching into the far future.

Solutions

  1. Clamp redemptionEnd to today.plus(MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1)).
  2. Compute the end as start.plus(MAX_REDEMPTION_DURATION) capped by the future limit.
  3. Surface the allowed maximum to clients so they request valid windows.

Example fix

// before
Instant end = start.plus(Duration.ofDays(365));
RedemptionRange.inclusive(clock, start, end);
// after
Instant end = start.plus(MAX_REDEMPTION_DURATION);
RedemptionRange.inclusive(clock, start, end);
Defensive patterns

Strategy: validation

Validate before calling

Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);
Instant maxEnd = today.plus(Duration.ofDays(MAX_WINDOW_DAYS)).plus(Duration.ofDays(1));
if (end.isAfter(maxEnd)) { end = maxEnd; }

Type guard

boolean isEndWithinPolicy(Instant end, Clock clock) {
  Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);
  return !end.isAfter(today.plus(MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1)));
}

Try / catch

try {
  RedemptionRange.inclusive(clock, start, end);
} catch (IllegalArgumentException e) {
  if (e.getMessage().contains("too far in the future")) {
    end = clampEnd(clock);
    return RedemptionRange.inclusive(clock, start, end);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling inclusive() with redemptionEnd > truncatedToday + MAX_REDEMPTION_DURATION + 1 day, e.g. pre-scheduling a redemption range months ahead.

Common situations: Generating long-lived default windows; client sending an end date far ahead; misinterpreting the allowed maximum window length constant.

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/5787edf2eebe049e. Report an issue: GitHub.

Appendix: source

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

      throws IllegalArgumentException {
    final Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);
    final Instant yesterday = today.minus(Duration.ofDays(1));

    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))

View on GitHub (pinned to 100ab61c82)