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

Invalid datetime format for

Error message

Invalid datetime format for %{attribute}

What it means

PeerReview::Validations#validate_peer_review_dates raises PeerReview::InvalidDatesError when a provided date field (unlock_at/due_at/lock_at) is a String that does not match Api::ISO8601_REGEX. The method accepts Time objects or ISO8601 strings; anything else in string form is rejected with the offending attribute named in the message.

Solutions

  1. Convert strings to ISO8601 (e.g. time.iso8601 or date.to_time.utc.iso8601) before passing.
  2. Pass Time/DateTime objects instead of strings to skip string parsing entirely.
  3. Pre-validate with Api::ISO8601_REGEX.match?(value) client-side and reject bad input early.

Example fix

// before
PeerReview::Validator.validate_peer_review_dates({ due_at: "03/01/2024 5pm" })
// after
PeerReview::Validator.validate_peer_review_dates({ due_at: Time.zone.parse("2024-03-01 17:00").iso8601 })
Defensive patterns

Strategy: validation

Validate before calling

def valid_peer_review_date?(v)
  v.respond_to?(:zone) || (v.is_a?(String) && Api::ISO8601_REGEX.match?(v))
end

Type guard

def coerce_to_time(v) = v.is_a?(String) ? Time.zone.parse(v) : v

Try / catch

begin
  validate_peer_review_dates(dates)
rescue PeerReview::InvalidDatesError => e
  render json: { error: e.message }, status: :bad_request
end

Prevention

When it happens

Trigger: Passing dates like "2024-03-01 10:00" (space instead of T), "03/01/2024", or "2024-03-01T10:00:00" with an offset format the regex rejects to peer_review_dates; localizing date strings before sending them.

Common situations: API clients using strftime with human-readable formats; form params passed through unprocessed; JS Date.toString() output sent directly to the endpoint.

Related errors


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

Appendix: source

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

  def validate_peer_review_sub_assignment_not_exist(assignment)
    if assignment.peer_review_sub_assignment.present?
      raise PeerReview::SubAssignmentExistsError, I18n.t("Peer review sub assignment exists")
    end
  end

  # Validates that peer review dates follow the restriction below
  # peer review unlock_at < peer review due_at <= peer review lock_at
  def validate_peer_review_dates(peer_review_dates)
    parsed_dates = {}

    %w[due_at unlock_at lock_at].each do |date_field|
      date_value = peer_review_dates.fetch(date_field.to_sym, nil)
      next unless date_value.present?

      # Accept both Time objects and ISO8601 strings for API compatibility
      if date_value.is_a?(String)
        unless Api::ISO8601_REGEX.match?(date_value)
          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")

View on GitHub (pinned to 1c9f0bb801)