instructure/canvas-lms · error · ActiveRecord::RecordNotFound

invalid assignment id #

Error message

invalid assignment id #{id}

What it means

Assignment::BulkUpdate#run processes a batch of date updates keyed by assignment id. When the id in the payload does not correspond to any assignment in the loaded assignments map (scope-limited to the course/current user's permissions), it raises ActiveRecord::RecordNotFound 'invalid assignment id <id>'. This prevents silently skipping or worse updating an assignment outside the intended scope.

Solutions

  1. Verify every id in the payload exists and is visible in the target course before submitting
  2. Refresh the assignments list before bulk edit to drop deleted rows
  3. Filter payload ids against Assignment.active.where(id: ids, course: course) client- or controller-side
  4. Rescue ActiveRecord::RecordNotFound and report which id was invalid instead of failing the whole batch blindly

Example fix

// before
Assignment::BulkUpdate.new(course, user, { "17" => dates }).run # 17 deleted
// after
valid_ids = course.assignments.active.where(id: [17]).pluck(:id)
payload.slice(*valid_ids.map(&:to_s)) then run
Defensive patterns

Strategy: try-catch

Validate before calling

const ids = payload.map(([id]) => id);
const valid = await canvas.get(`/courses/${courseId}/assignments`, { ids });
if (valid.length !== ids.length) throw new Error('Some assignments no longer exist');

Type guard

const assignmentExists = (id, assignments) => assignments.some(a => String(a.id) === String(id))

Try / catch

begin
  Assignment::BulkUpdate.new(course, user, payload).run
rescue ActiveRecord::RecordNotFound => e
  render json: { error: e.message }, status: :not_found
end

Prevention

When it happens

Trigger: POSTing to the assignment bulk-update endpoint with an assignment id from another course, a deleted assignment, or one the user cannot read; stale client state after an assignment was deleted; id passed as a non-numeric string so id.to_i mismatches.

Common situations: Frontend batch-edit table holding stale rows after concurrent deletion; users tampering with ids; cross-shard/cross-course id reuse in scripts hitting the API.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at app/models/assignment/bulk_update.rb:45

  delegate :grading_periods?, to: :@context

  def run(progress, assignment_data)
    # assignment_data looks like [:id, :all_dates => [:id, :base, :due_at, :unlock_at, :lock_at]]
    assignment_data_hash = assignment_data.index_by { |a| a["id"] }
    assignments = @context.active_assignments.where(id: assignment_data_hash.keys).preload(:assignment_overrides).index_by(&:id)
    assignments_to_save = Set.new

    # 1. update AR models (in memory!)
    assignment_data_hash.each do |id, data|
      dates = data["all_dates"]
      next unless dates.present?

      base, overrides = dates.partition { |date| date["base"] }

      # 1a. update the assignment
      assignment = assignments[id.to_i]
      raise ActiveRecord::RecordNotFound, "invalid assignment id #{id}" unless assignment

      if base.any?
        assignment.content_being_saved_by(@current_user)
        assignment.updating_user = @current_user
        assignment.assign_attributes(base.first.slice(*%w[due_at unlock_at lock_at]))
        assignments_to_save << assignment if assignment.changed?
      end

      # 1b. update associated overrides
      overrides.each do |override_data|
        override = assignment.assignment_overrides.detect { |o| o.id == override_data["id"].to_i }
        raise ActiveRecord::RecordNotFound, "invalid assignment override id #{override_data["id"]} for assignment #{assignment.id}" unless override

        %w[due_at unlock_at lock_at].each do |date|
          if override_data.key?(date)
            override.send(:"#{date}=", override_data[date])
            override.send(:"#{date}_overridden=", true)
          else

View on GitHub (pinned to 1c9f0bb801)