{"record":{"id":"7b8382b7de8d84d9","repo":"signalapp/Signal-Server","slug":"end-of-range-must-be-after-start-of-range","errorCode":null,"errorMessage":"end of range must be after start of range","messagePattern":"end of range must be after start of range","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/auth/RedemptionRange.java","lineNumber":55,"sourceCode":"  ///   - `redemptionEnd` >= `redemptionStart`\n  ///   - `redemptionStart` and `redemptionEnd` are day-aligned\n  ///   - `redemptionStart` is yesterday or later\n  ///   - `redemptionEnd` is tomorrow + `MAX_REDEMPTION_DURATION` or earlier\n  ///   - The number of days requested is less than `MAX_REDEMPTION_DURATION`\n  ///\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    }","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/auth/RedemptionRange.java#L37-L73","documentation":"RedemptionRange.inclusive() validates a [start, end] instant range for receipt redemption. It throws IllegalArgumentException when redemptionStart is strictly after redemptionEnd, i.e. the caller supplied a backwards range.","triggerScenarios":"Calling RedemptionRange.inclusive(clock, start, end) with start > end (even by milliseconds; equality is allowed).","commonSituations":"Swapped order of query parameters when computing a range from client input; timezone arithmetic producing an end earlier than start; off-by-one day when building ranges.","solutions":["Swap or re-order the arguments so start <= end before calling inclusive().","Validate/normalize client-supplied dates before constructing the range.","Use Instant.isAfter as a pre-check and return a 400 to the API caller."],"exampleFix":"// before\nRedemptionRange.inclusive(clock, endDate, startDate);\n// after\nif (startDate.isAfter(endDate)) {\n  throw new BadRequestException(\"start must not be after end\");\n}\nRedemptionRange.inclusive(clock, startDate, endDate);","handlingStrategy":"validation","validationCode":"if (start.isAfter(end)) {\n  throw new IllegalArgumentException(\"start must not be after end\");\n}","typeGuard":"boolean isValidRange(Instant start, Instant end) {\n  return start != null && end != null && !start.isAfter(end);\n}","tryCatchPattern":"try {\n  RedemptionRange range = RedemptionRange.inclusive(clock, start, end);\n} catch (IllegalArgumentException e) {\n  return Response.status(400, e.getMessage()).build();\n}","preventionTips":["Normalize start/end ordering as min/max before constructing ranges","Validate date-range request parameters at the resource layer","Use a shared range-builder helper so ordering bugs cannot recur"],"tags":["validation","date-range","illegal-argument"],"backgroundTag":"invalid-argument-value","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"}