instructure/canvas-lms · error
content is not a Submission
Error message
content is not a Submission
What it means
ContentParticipation.submission_read_state requires its content argument to be a Submission. Raising "content is not a Submission" protects the participation-read-state lookup, which queries ContentParticipation scoped by content.
Solutions
- Pass the Submission record (e.g. assignment.submissions.find_by(user:)) instead of the assignment or discussion entry
- Guard the call site so only Submission instances reach submission_read_state
- If tracking participation for other content types, use the appropriate participation class for that type
Example fix
# before ContentParticipation.submission_read_state(content: assignment, user: student) # after submission = assignment.submissions.find_by!(user: student) ContentParticipation.submission_read_state(content: submission, user: student)
Defensive patterns
Strategy: type-guard
Validate before calling
raise 'expected Submission' unless content.is_a?(Submission)
Type guard
def submission?(obj) = obj.is_a?(Submission)
Try / catch
begin
ContentParticipation.submission_read_state(content:, user:)
rescue RuntimeError => e
raise e unless e.message == 'content is not a Submission'
Rails.logger.warn('submission_read_state called with non-Submission')
end Prevention
- Resolve the Submission (assignment.submissions.find_by(user:)) rather than passing the assignment
- Type-check parameters at call sites in generic read-state helpers
- Keep content-participation calls out of polymorphic code paths that pass mixed models
When it happens
Trigger: Calling submission_read_state (or submission_item_read?) with a DiscussionEntry, Assignment, or other non-Submission object as content:; passing a nil content; refactor renamed parameters and callers pass the wrong record.
Common situations: Generic read-state code reused for multiple content types and passed the wrong model; seed/data-fix scripts passing an assignment instead of its submission; view/controller passing the wrong association (submission vs submission.history etc.).
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- # is invalid
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
- A student referenced a non-existent user #
- A user did not pass validation
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/48ed646d9b25cfd9.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/content_participation.rb:146
content.content_participations.build(user:, workflow_state:, content_item:)
end
private_class_method :build_item
def self.same_workflow_state?(participant, workflow_state)
participant.present? && participant.workflow_state == workflow_state
end
private_class_method :same_workflow_state?
def self.submission_read?(content:, user:)
submission_read_state(content, user) != "unread"
end
def self.submission_item_read?(content:, user:, content_item:)
submission_read_state(content, user, content_item) != "unread"
end
def self.submission_read_state(content, user, content_item = nil)
raise "content is not a Submission" unless content.is_a?(Submission)
raise "#{content_item} is invalid" if content_item.present? && !CONTENT_ITEMS.include?(content_item)
states = if content_item.present?
ContentParticipation.where(content:, user:, content_item:).pluck(:workflow_state)
else
ContentParticipation.where(content:, user:).pluck(:workflow_state)
end
return nil if states.empty?
return "unread" if states.any?("unread")
"read"
end
def self.items_by_submission(participations, workflow_state)
unread_items = {}
participations.each do |cp|View on GitHub (pinned to 1c9f0bb801)