{"record":{"id":"6dbbb64358c073bd","repo":"signalapp/Signal-Server","slug":"start-of-range-too-far-in-the-past","errorCode":null,"errorMessage":"start of range too far in the past","messagePattern":"start of range too far in the past","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/auth/RedemptionRange.java","lineNumber":64,"sourceCode":"  /// @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  }\n\n  @Override\n  public @NotNull Iterator<Instant> iterator() {\n    final Instant fromInstant = from.atStartOfDay(ZoneOffset.UTC).toInstant();","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/auth/RedemptionRange.java#L46-L82","documentation":"RedemptionRange.inclusive() rejects ranges whose start is earlier than yesterday (today truncated minus one day, evaluated on the provided Clock). This bounds how far back a receipt-redemption query may reach.","triggerScenarios":"Calling inclusive() with a redemptionStart more than one day before the clock's current (truncated) day, e.g. querying a week-old redemption window.","commonSituations":"Backfill jobs trying to redeem old receipts; clients caching old start dates; unit tests using fixed instants far in the past while the injected Clock is real-time.","solutions":["Clamp redemptionStart to at minimum yesterday: max(requestedStart, today.minus(1 day)).","If historical lookups are required, use a different API or change the policy constant; do not bypass the check.","In tests, inject a frozen Clock consistent with the instants under test."],"exampleFix":"// before\nRedemptionRange.inclusive(clock, weekAgo, today);\n// after\nInstant today = clock.instant().truncatedTo(ChronoUnit.DAYS);\nInstant start = Instant.from(weekAgo).isBefore(today.minus(Duration.ofDays(1)))\n    ? today.minus(Duration.ofDays(1)) : weekAgo;\nRedemptionRange.inclusive(clock, start, today);","handlingStrategy":"validation","validationCode":"Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);\nif (start.isBefore(today.minus(Duration.ofDays(1)))) {\n  start = today.minus(Duration.ofDays(1));\n}","typeGuard":"boolean isStartWithinPolicy(Instant start, Clock clock) {\n  return !start.isBefore(clock.instant().truncatedTo(ChronoUnit.DAYS).minus(Duration.ofDays(1)));\n}","tryCatchPattern":"try {\n  RedemptionRange.inclusive(clock, start, end);\n} catch (IllegalArgumentException e) {\n  if (e.getMessage().contains(\"too far in the past\")) {\n    return redeemMostRecentAllowedWindow(clock);\n  }\n  throw e;\n}","preventionTips":["Clamp requested start dates to the policy minimum before calling the API","Inject a consistent Clock in tests matching the fixtures' dates","Communicate the maximum lookback window to API consumers"],"tags":["validation","date-range","bounds-check"],"backgroundTag":"value-out-of-range","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"}