{"record":{"id":"6a38406f3033183b","repo":"signalapp/Signal-Server","slug":"timestamps-must-be-day-aligned","errorCode":null,"errorMessage":"timestamps must be day aligned","messagePattern":"timestamps must be day aligned","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/auth/RedemptionRange.java","lineNumber":60,"sourceCode":"  ///\n  /// @param clock           Clock to use to get current day\n  /// @param redemptionStart The first day included in the range\n  /// @param redemptionEnd   The last day included in the range\n  /// @return A {@link RedemptionRange} that can be used to iterate each day between `redemptionStart` and\n  ///  `redemptionEnd`\n  /// @throws IllegalArgumentException if the redemption bounds were not valid\n  public static RedemptionRange inclusive(Clock clock, Instant redemptionStart, Instant redemptionEnd)\n      throws IllegalArgumentException {\n    final Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);\n    final Instant yesterday = today.minus(Duration.ofDays(1));\n\n    if (redemptionStart.isAfter(redemptionEnd)) {\n      throw new IllegalArgumentException(\"end of range must be after start of range\");\n    }\n\n    if (!redemptionStart.truncatedTo(ChronoUnit.DAYS).equals(redemptionStart)\n        || !redemptionEnd.truncatedTo(ChronoUnit.DAYS).equals(redemptionEnd)) {\n      throw new IllegalArgumentException(\"timestamps must be day aligned\");\n    }\n\n    if (redemptionStart.isBefore(yesterday)) {\n      throw new IllegalArgumentException(\"start of range too far in the past\");\n    }\n\n    if (redemptionEnd.isAfter(today.plus(MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1)))) {\n      throw new IllegalArgumentException(\"end of range too far in the future\");\n    }\n\n    if (redemptionEnd.isAfter(redemptionStart.plus(MAX_REDEMPTION_DURATION))) {\n      throw new IllegalArgumentException(\"redemption window too large\");\n    }\n\n    return new RedemptionRange(\n        LocalDate.ofInstant(redemptionStart, ZoneOffset.UTC),\n        LocalDate.ofInstant(redemptionEnd, ZoneOffset.UTC));\n  }","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/auth/RedemptionRange.java#L42-L78","documentation":"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.","triggerScenarios":"Calling inclusive() with Instants like Instant.parse(\"2024-03-05T10:30:00Z\") instead of midnight-aligned instants such as 2024-03-05T00:00:00Z.","commonSituations":"Passing raw request timestamps ('now') instead of day boundaries; forgetting to truncate client-provided dates; converting local dates to instants without normalizing to midnight.","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."],"exampleFix":"// before\nRedemptionRange.inclusive(clock, Instant.now(), Instant.now().plus(Duration.ofDays(7)));\n// after\nInstant start = Instant.now().truncatedTo(ChronoUnit.DAYS);\nInstant end = start.plus(Duration.ofDays(7));\nRedemptionRange.inclusive(clock, start, end);","handlingStrategy":"validation","validationCode":"Instant s = start.truncatedTo(ChronoUnit.DAYS);\nInstant e = end.truncatedTo(ChronoUnit.DAYS);\nif (!s.equals(start) || !e.equals(end)) {\n  start = s; end = e; // normalize before calling\n}","typeGuard":"boolean isDayAligned(Instant t) {\n  return t.equals(t.truncatedTo(ChronoUnit.DAYS));\n}","tryCatchPattern":"try {\n  RedemptionRange.inclusive(clock, start, end);\n} catch (IllegalArgumentException e) {\n  if (e.getMessage().contains(\"day aligned\")) {\n    return Response.status(400, \"timestamps must be UTC midnight\").build();\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["validation","date-range","day-alignment"],"backgroundTag":"invalid-date-format","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}