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
- Truncate both instants: redemptionStart.truncatedTo(ChronoUnit.DAYS) before the call.
- Construct endpoints from LocalDate.atStartOfDay(ZoneOffset.UTC).toInstant().
- 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
- Always build range endpoints with LocalDate.atStartOfDay(ZoneOffset.UTC).toInstant()
- Never pass Instant.now() or raw client timestamps directly as range endpoints
- Document the day-alignment requirement in your API schema
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
- end of range must be after start of range
- start of range too far in the past
- end of range too far in the future
- redemption window too large
- Blank header
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)