{"record":{"id":"a4af3e91aa1418fc","repo":"instructure/canvas-lms","slug":"invalid-assignment-id-id","errorCode":null,"errorMessage":"invalid assignment id #{id}","messagePattern":"invalid assignment id #(.+?)","errorType":"exception","errorClass":"ActiveRecord::RecordNotFound","httpStatus":404,"severity":"error","filePath":"app/models/assignment/bulk_update.rb","lineNumber":45,"sourceCode":"\n  delegate :grading_periods?, to: :@context\n\n  def run(progress, assignment_data)\n    # assignment_data looks like [:id, :all_dates => [:id, :base, :due_at, :unlock_at, :lock_at]]\n    assignment_data_hash = assignment_data.index_by { |a| a[\"id\"] }\n    assignments = @context.active_assignments.where(id: assignment_data_hash.keys).preload(:assignment_overrides).index_by(&:id)\n    assignments_to_save = Set.new\n\n    # 1. update AR models (in memory!)\n    assignment_data_hash.each do |id, data|\n      dates = data[\"all_dates\"]\n      next unless dates.present?\n\n      base, overrides = dates.partition { |date| date[\"base\"] }\n\n      # 1a. update the assignment\n      assignment = assignments[id.to_i]\n      raise ActiveRecord::RecordNotFound, \"invalid assignment id #{id}\" unless assignment\n\n      if base.any?\n        assignment.content_being_saved_by(@current_user)\n        assignment.updating_user = @current_user\n        assignment.assign_attributes(base.first.slice(*%w[due_at unlock_at lock_at]))\n        assignments_to_save << assignment if assignment.changed?\n      end\n\n      # 1b. update associated overrides\n      overrides.each do |override_data|\n        override = assignment.assignment_overrides.detect { |o| o.id == override_data[\"id\"].to_i }\n        raise ActiveRecord::RecordNotFound, \"invalid assignment override id #{override_data[\"id\"]} for assignment #{assignment.id}\" unless override\n\n        %w[due_at unlock_at lock_at].each do |date|\n          if override_data.key?(date)\n            override.send(:\"#{date}=\", override_data[date])\n            override.send(:\"#{date}_overridden=\", true)\n          else","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/assignment/bulk_update.rb#L27-L63","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify every id in the payload exists and is visible in the target course before submitting","Refresh the assignments list before bulk edit to drop deleted rows","Filter payload ids against Assignment.active.where(id: ids, course: course) client- or controller-side","Rescue ActiveRecord::RecordNotFound and report which id was invalid instead of failing the whole batch blindly"],"exampleFix":"// before\nAssignment::BulkUpdate.new(course, user, { \"17\" => dates }).run # 17 deleted\n// after\nvalid_ids = course.assignments.active.where(id: [17]).pluck(:id)\npayload.slice(*valid_ids.map(&:to_s)) then run","handlingStrategy":"try-catch","validationCode":"const ids = payload.map(([id]) => id);\nconst valid = await canvas.get(`/courses/${courseId}/assignments`, { ids });\nif (valid.length !== ids.length) throw new Error('Some assignments no longer exist');","typeGuard":"const assignmentExists = (id, assignments) => assignments.some(a => String(a.id) === String(id))","tryCatchPattern":"begin\n  Assignment::BulkUpdate.new(course, user, payload).run\nrescue ActiveRecord::RecordNotFound => e\n  render json: { error: e.message }, status: :not_found\nend","preventionTips":["Refresh the assignments list before opening the bulk edit UI","Scope payload ids to course.assignments.active ids server-side","Handle per-row failures instead of failing the whole batch"],"tags":["activerecord","bulk-update","record-not-found"],"backgroundTag":"record-not-found","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}