instructure/canvas-lms · error · RuntimeError

cannot change column: captions - locked by Master Course

Error message

cannot change column: captions - locked by Master Course

What it means

MediaTrack#check_for_restricted_updates runs as a validation before save. If the media track's attachment belongs to child content locked by a master course's :content restrictions, any change (e.g. uploading new captions) is blocked with 'cannot change column: captions - locked by Master Course'.

Solutions

  1. Unlock the related content in the master course (remove content lock) so child captions can change
  2. Make the caption edit in the master course and sync to associated courses
  3. If legitimate (e.g. migration/import), ensure skip_restrictions? applies (set @importing_migration or @skip_downstream_changes)

Example fix

// before
attachment.media_tracks.create!(...)
// after
unless attachment.child_content_restrictions&.dig(:content)
  attachment.media_tracks.create!(...)
end
Defensive patterns

Strategy: try-catch

Validate before calling

if track.attachment&.child_content_restrictions&.dig(:content)
  # surface UI message instead of attempting save
end

Try / catch

begin
  media_track.save!
rescue RuntimeError => e
  raise unless e.message.include?('locked by Master Course')
  flash[:error] = I18n.t('Captions are locked by the master course')
end

Prevention

When it happens

Trigger: Creating/updating a MediaTrack whose attachment.child_content_restrictions includes :content while the object is child content of a blueprint course and restrictions are not skipped.

Common situations: Uploading replacement captions for a media file attached to a locked blueprint course item; imports touching master-locked media; users editing captions in a locked child course section.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

Thrown at app/models/media_track.rb:65

  validates :kind, inclusion: { in: %w[subtitles captions descriptions chapters metadata] }
  validates :content, presence: true, unless: -> { it.asr? && (it.processing? || it.failed?) }
  validates :locale, format: { with: /\A[A-Za-z-]+\z/ }, uniqueness: { scope: :attachment_id, unless: -> { it.attachment_id.blank? } }
  restrict_columns :content, %i[attachment_id content locale media_object_id webvtt_content external_id]

  RE_LOOKS_LIKE_TTML = /<tt\s+xml/i
  validates :content, format: {
    without: RE_LOOKS_LIKE_TTML,
    message: ->(_object, _data) { t("TTML tracks are not allowed because they are susceptible to xss attacks") }
  }

  # MasterCourses::CollectionRestrictor handles soft-deletes, but doesn't handle
  # hard deletes well. One day we  might want to standardize this to more hard
  # deleted objects.
  def check_for_restricted_updates
    return true if skip_restrictions? || attachment&.skip_restrictions?
    return unless attachment&.child_content_restrictions&.dig(:content)

    raise "cannot change column: captions - locked by Master Course"
  end

  def set_media_and_attachment
    self.attachment_id ||= media_object.attachment_id
    self.media_object_id ||= attachment.media_object_by_media_id
  end

  def webvtt_content
    super || content
  end

  def convert_srt_to_wvtt
    return if content.blank?

    if content.exclude?("WEBVTT") && (content_changed? || self["webvtt_content"].nil?)
      srt_content = content.dup
      srt_content.gsub!(/(:|^)(\d)(,|:)/, '\10\2\3')
      srt_content.gsub!(/([0-9]{2}:[0-9]{2}:[0-9]{2})(,)([0-9]{3})/, '\1.\3')

View on GitHub (pinned to 1c9f0bb801)