instructure/canvas-lms · error · UnknownTypeError

Unable to determine asset type for Lti::Asset id=#

Error message

Unable to determine asset type for Lti::Asset id=#{id}. The referred discussion_entry_version or the submission has been probably deleted.

What it means

Lti::Asset#asset_type infers the asset's type from whichever foreign key is present (submission, attachment, discussion_entry_version). If none of the referenced records' ids is set — typically because the underlying submission or discussion_entry_version was deleted — it logs the anomaly and raises UnknownTypeError, since an Lti::Asset with no resolvable backing record cannot be typed.

Solutions

  1. Purge or ignore Lti::Asset rows whose backing submission/discussion_entry_version no longer exists; guard callers with asset-aware existence checks.
  2. Restore the deleted submission/discussion entry if the data should exist.
  3. Wrap asset_type access in a rescue of Lti::Asset::UnknownTypeError and skip the asset.
  4. Add cleanup callbacks so deleting a Submission/DiscussionEntryVersion also deletes dependent Lti::Asset rows.

Example fix

// before
atype = asset.asset_type
// after
atype = asset.asset_type
rescue Lti::Asset::UnknownTypeError => e
  Rails.logger.warn("Skipping stale Lti::Asset: #{e.message}")
  next
Defensive patterns

Strategy: try-catch

Validate before calling

next if asset.submission_id.blank? && asset.attachment_id.blank? && asset.discussion_entry_version_id.blank?
atype = asset.asset_type

Type guard

def resolvable?(asset) = asset.submission_id.present? || asset.attachment_id.present? || asset.discussion_entry_version_id.present?

Try / catch

begin
  atype = asset.asset_type
rescue Lti::Asset::UnknownTypeError => e
  Rails.logger.warn("Skipping stale asset: #{e.message}")
  next
end

Prevention

When it happens

Trigger: Reading an Lti::Asset whose submission_id, attachment_id, and discussion_entry_version_id are all blank — e.g. after the Submission or DiscussionEntryVersion it pointed at was soft/hard deleted while the asset row (or a reference to it) persists.

Common situations: AGS/LTI score reporting for a submission deleted by a student or cleanup job; discussion topics/entries removed while LTI assets remain; speedgrader or analytics jobs touching stale assets.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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

Appendix: source

Thrown at app/models/lti/asset.rb:120

      # In theory, submissions can be deleted, to not break the foreign key constraint we set the submission_id to null
      # when the submission is hard deleted with dependent: :nullify in the Submission model.
      return "deleted"
    end

    # if submission_attempt is set, it's RCE content
    if submission_attempt.present?
      TYPE_TEXT_ENTRY
    # if attachment_id is set, it's a file attachment of a submission
    elsif attachment_id.present?
      TYPE_ATTACHMENT
    # if discussion_entry_version_id is set, it's a discussion entry (comment)
    elsif discussion_entry_version_id.present?
      TYPE_DISCUSSION_ENTRY
    else
      Rails.logger.error(
        "Lti::Asset unknown type id=#{id}, submission_id=#{submission_id}, attachment_id=#{attachment_id}, submission_attempt=#{submission_attempt}, discussion_entry_version_id=#{discussion_entry_version_id}"
      )
      raise UnknownTypeError, "Unable to determine asset type for Lti::Asset id=#{id}. The referred discussion_entry_version or the submission has been probably deleted."
    end
  end

  # Returns the submission claim ID for this asset.
  # Discussion entries use DiscussionEntryVersion#lti_submission_claim_id.
  # All other types delegate to Submission#lti_attempt_id with the asset's attempt number.
  def submission_lti_claim_id
    return nil unless submission

    # Discussion Entry
    if discussion_entry?
      discussion_entry_version.lti_submission_claim_id(submission)
    # Attachment of discussion entry
    elsif attachment_id.present? && submission.submission_type == "discussion_topic"
      # attachment is stored on discussion_entry not discussion_entry_version,
      # so we find the most recent entry and version for the submission that contains the attachment
      # and send that submission in the claim (also that is what we show in SG)
      entry = DiscussionEntry.where(

View on GitHub (pinned to 1c9f0bb801)