instructure/canvas-lms · error · PeerReview::InvalidDatesError
Due date cannot be after until date
Error message
Due date cannot be after until date
What it means
PeerReview::InvalidDatesError raised by validate_peer_review_dates when a peer review sub-assignment's due_at is later than its lock_at (until date). Canvas enforces unlock_at <= due_at <= lock_at on peer review dates, so a due date after the until date is incoherent — submissions could still be due after reviews were locked. It is thrown during creation/update of peer review sub assignments via validate_peer_review_dates_against_parent_assignment.
Solutions
- Compare the payload's due_at and lock_at; move lock_at to a timestamp >= due_at, or lower due_at
- If dates were swapped by mistake, exchange the due_at and lock_at values
- If only a due date is wanted, omit lock_at (nil lock_at skips this check)
- Validate in the client before submitting: raise unless Time.parse(due_at) <= Time.parse(lock_at)
Example fix
// before
peer_review_dates = { due_at: '2026-05-10T23:59:00Z', lock_at: '2026-05-05T23:59:00Z' }
// after
peer_review_dates = { due_at: '2026-05-05T23:59:00Z', lock_at: '2026-05-10T23:59:00Z' } Defensive patterns
Strategy: validation
Validate before calling
def valid_peer_review_dates?(dates)
due = dates[:due_at]&.then { |v| v.is_a?(String) ? Time.zone.parse(v) : v }
lock = dates[:lock_at]&.then { |v| v.is_a?(String) ? Time.zone.parse(v) : v }
!(due && lock && due > lock)
end Type guard
def as_time(v) return v if v.is_a?(Time || ActiveSupport::TimeWithZone) Time.zone.parse(v) if v.is_a?(String) && Api::ISO8601_REGEX.match?(v) end
Try / catch
begin
service.create_peer_review_with_dates(dates)
rescue PeerReview::InvalidDatesError => e
render json: { error: e.message }, status: :bad_request
end Prevention
- Always keep the ordering unlock_at <= due_at <= lock_at when setting peer review dates
- Compute lock_at as due_at + buffer, never an independent earlier value
- Use ISO8601 UTC strings consistently to avoid silent timezone comparisons
- Add a client-side date picker constraint binding lock_at min to due_at
When it happens
Trigger: Calling the peer review creation/update API (e.g. POST/PUT that builds the peer_review_sub_assignment) with due_at set to a timestamp strictly greater than the supplied lock_at, while both are present. E.g. due_at: '2026-05-01T00:00:00Z', lock_at: '2026-04-30T00:00:00Z'.
Common situations: Admins copy an assignment's dates into a script and swap due/lock fields; clients that compute lock_at from due_at with an off-by-one-day offset; time zone mishandling making an intended-equal pair compare as due > lock.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Available from date cannot be after until date
- Invalid date format for
- Parent override due date cannot be before parent override…
- Peer review due date cannot be before assignment due date
- Peer review override available from date cannot be before…
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/9777c3f1a614a9d7.
Report an issue: GitHub.
Appendix: source
Thrown at app/services/peer_review/validations.rb:95
raise PeerReview::InvalidDatesError, I18n.t("Invalid datetime format for %{attribute}", attribute: date_field)
end
parsed_dates[date_field.to_sym] = Time.zone.parse(date_value)
else
parsed_dates[date_field.to_sym] = date_value
end
end
due_at = parsed_dates[:due_at]
unlock_at = parsed_dates[:unlock_at]
lock_at = parsed_dates[:lock_at]
if due_at && unlock_at && due_at < unlock_at
raise PeerReview::InvalidDatesError, I18n.t("Due date cannot be before available from date")
end
if due_at && lock_at && due_at > lock_at
raise PeerReview::InvalidDatesError, I18n.t("Due date cannot be after until date")
end
if unlock_at && lock_at && unlock_at > lock_at
raise PeerReview::InvalidDatesError, I18n.t("Available from date cannot be after until date")
end
end
# Validates that peer review override dates fall within parent assignment override dates
# assignment available from date <= assignment due date <= peer review available from date <= peer review due date <= peer review until date <= assignment until date
def validate_override_dates_against_parent_override(peer_review_override, parent_override)
parent_unlock_at = parent_override.unlock_at_overridden ? parent_override.unlock_at : nil
parent_due_at = parent_override.due_at_overridden ? parent_override.due_at : nil
parent_lock_at = parent_override.lock_at_overridden ? parent_override.lock_at : nil
validate_dates_within_parent_boundaries(
child_dates: peer_review_override,
parent_unlock_at:,
parent_due_at:,View on GitHub (pinned to 1c9f0bb801)