instructure/canvas-lms · error · PeerReview::InvalidDatesError

Invalid date format for

Error message

Invalid date format for %{field}: %{value}

What it means

PeerReview::InvalidDatesError raised by parse_date when a date field in the child dates hash is a String that Time.zone.parse cannot convert to a Time (returns nil). Unlike validate_peer_review_dates (which uses Api::ISO8601_REGEX), parse_date relies on Rails' time parser, so only strings producing nil fail here — unparseable garbage, not merely non-ISO8601 formats. Thrown from validate_dates_within_parent_boundaries during peer review vs parent date validation.

Solutions

  1. Send ISO8601/ISO8601-formatted UTC timestamps, e.g. '2026-05-01T00:00:00Z'
  2. Pre-parse strings to Time objects client-side before passing the dates hash
  3. Log the %{field} and %{value} in the message to find the offending key and fix the producer
  4. Sanitize inputs: drop blank or non-date strings before calling the validator

Example fix

// before
validate_peer_review_dates_against_parent_assignment({ due_at: 'March 3rd, 2026 sometime' }, assignment)
// after
validate_peer_review_dates_against_parent_assignment({ due_at: '2026-03-03T00:00:00Z' }, assignment)
Defensive patterns

Strategy: type-guard

Validate before calling

def ensure_parseable_dates(dates)
  dates.each do |field, value|
    next if value.nil? || value.is_a?(Time)
    raise ArgumentError, "#{field} unparseable: #{value}" if Time.zone.parse(value).nil?
  end
end

Type guard

def coercible_date?(v)
  v.is_a?(Time) || (v.is_a?(String) && !Time.zone.parse(v).nil?)
end

Try / catch

begin
  validator.validate_peer_review_dates_against_parent_assignment(dates, assignment)
rescue PeerReview::InvalidDatesError => e
  if e.message.start_with?('Invalid date format')
    bad = e.message[/value: (.+)$/, 1]
    dates.reject! { |_, v| v == bad }
    retry
  end
  raise
end

Prevention

When it happens

Trigger: Calling validate_peer_review_dates_against_parent_assignment or validate_override_dates_against_parent_override with a dates hash containing e.g. due_at: 'not-a-date', '32/13/2026', or another string Time.zone.parse cannot interpret.

Common situations: API clients sending localized date formats ('01/02/2026' ambiguity aside, some locales produce nil); empty-ish strings that pass present? but fail parsing; deserialization bugs delivering placeholder text like 'null' or 'TBD' as the date value.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/a5c018745daec4bb. Report an issue: GitHub.

Appendix: source

Thrown at app/services/peer_review/validations.rb:138

    validate_peer_review_dates(peer_review_dates)

    validate_dates_within_parent_boundaries(
      child_dates: peer_review_dates,
      parent_unlock_at: parent_assignment.unlock_at,
      parent_due_at: parent_assignment.due_at,
      parent_lock_at: parent_assignment.lock_at,
      is_override: false
    )
  end

  def parse_date(dates_hash, field)
    date_value = dates_hash.fetch(field, nil)
    return nil unless date_value.present?

    if date_value.is_a?(String)
      parsed = Time.zone.parse(date_value)
      if parsed.nil?
        raise PeerReview::InvalidDatesError,
              I18n.t("Invalid date format for %{field}: %{value}", field:, value: date_value)
      end
      parsed
    else
      date_value
    end
  end

  def validate_dates_within_parent_boundaries(child_dates:, parent_unlock_at:, parent_due_at:, parent_lock_at:, is_override: false)
    child_unlock_at = parse_date(child_dates, :unlock_at)
    child_due_at = parse_date(child_dates, :due_at)
    child_lock_at = parse_date(child_dates, :lock_at)

    # Validate: parent unlock_at <= parent due_at
    if parent_unlock_at && parent_due_at && parent_unlock_at > parent_due_at
      raise PeerReview::InvalidDatesError,
            is_override ? I18n.t("Parent override due date cannot be before parent override available from date") : I18n.t("Assignment due date cannot be before assignment available from date")
    end

View on GitHub (pinned to 1c9f0bb801)