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

  1. Compare the payload's due_at and lock_at; move lock_at to a timestamp >= due_at, or lower due_at
  2. If dates were swapped by mistake, exchange the due_at and lock_at values
  3. If only a due date is wanted, omit lock_at (nil lock_at skips this check)
  4. 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

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


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)