signalapp/Signal-Server · error · IllegalArgumentException

timestamps must be day aligned

Error message

timestamps must be day aligned

What it means

Thrown by RedemptionRange.inclusive as a validation guard when the supplied redemptionStart or redemptionEnd Instant is not truncated to a UTC day boundary (i.e., has a non-zero time-of-day component). The range is meant to iterate whole days, so only day-aligned timestamps are accepted; any caller passing an instant with intra-day time triggers this IllegalArgumentException.

Solutions

  1. Truncate both instants: redemptionStart.truncatedTo(ChronoUnit.DAYS) before the call.
  2. Construct endpoints from LocalDate.atStartOfDay(ZoneOffset.UTC).toInstant().
  3. Reject non-aligned client input with a clear 400 message before calling the API.

Example fix

// before
RedemptionRange.inclusive(clock, Instant.now(), Instant.now().plus(Duration.ofDays(7)));
// after
Instant start = Instant.now().truncatedTo(ChronoUnit.DAYS);
Instant end = start.plus(Duration.ofDays(7));
RedemptionRange.inclusive(clock, start, end);
Defensive patterns

Strategy: validation

Validate before calling

Instant s = start.truncatedTo(ChronoUnit.DAYS);
Instant e = end.truncatedTo(ChronoUnit.DAYS);
if (!s.equals(start) || !e.equals(end)) {
  start = s; end = e; // normalize before calling
}

Type guard

boolean isDayAligned(Instant t) {
  return t.equals(t.truncatedTo(ChronoUnit.DAYS));
}

Try / catch

try {
  RedemptionRange.inclusive(clock, start, end);
} catch (IllegalArgumentException e) {
  if (e.getMessage().contains("day aligned")) {
    return Response.status(400, "timestamps must be UTC midnight").build();
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling inclusive() with Instants like Instant.parse("2024-03-05T10:30:00Z") instead of midnight-aligned instants such as 2024-03-05T00:00:00Z.

Common situations: Passing raw request timestamps ('now') instead of day boundaries; forgetting to truncate client-provided dates; converting local dates to instants without normalizing to midnight.

Related errors


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

Appendix: source

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

  ///
  /// @param clock           Clock to use to get current day
  /// @param redemptionStart The first day included in the range
  /// @param redemptionEnd   The last day included in the range
  /// @return A {@link RedemptionRange} that can be used to iterate each day between `redemptionStart` and
  ///  `redemptionEnd`
  /// @throws IllegalArgumentException if the redemption bounds were not valid
  public static RedemptionRange inclusive(Clock clock, Instant redemptionStart, Instant redemptionEnd)
      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));
  }

View on GitHub (pinned to 100ab61c82)