{"record":{"id":"214c05c9d351e0c7","repo":"instructure/canvas-lms","slug":"invalid-association","errorCode":null,"errorMessage":"invalid association","messagePattern":"invalid association","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/models/course.rb","lineNumber":4591,"sourceCode":"    # RUBY 2.7 this can go away (**{} will work at the caller)\n    raise ArgumentError, \"Only send one hash\" if !changes.empty? && !kwargs.empty?\n\n    changes = kwargs if changes.empty? && !kwargs.empty?\n\n    if changes[:is_public] || changes[:is_public_to_auth_users]\n      assignments.touch_all\n      attachments.touch_all\n      calendar_events.touch_all\n      context_modules.touch_all\n      discussion_topics.touch_all\n      quizzes.touch_all\n      wiki.touch\n      wiki_pages.touch_all\n    end\n  end\n\n  def clear_todo_list_cache_later(association_type)\n    raise \"invalid association\" unless association(association_type).klass == User\n\n    delay(run_at: 15.seconds.from_now, singleton: \"course_clear_cache_#{global_id}_#{association_type}\", on_conflict: :loose)\n      .clear_todo_list_cache(association_type)\n  end\n\n  def clear_todo_list_cache(association_type)\n    raise \"invalid association\" unless association(association_type).klass == User\n\n    send(association_type).clear_cache_keys(:todo_list)\n  end\n\n  def touch_admins # TODO: remove after existing jobs run\n    clear_todo_list_cache(:admins)\n  end\n\n  def clear_caches_if_necessary\n    clear_cache_key(:account_associations) if saved_change_to_root_account_id? || saved_change_to_account_id?\n  end","sourceCodeStart":4573,"sourceCodeEnd":4609,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/course.rb#L4573-L4609","documentation":"Course#clear_todo_list_cache_later validates that the association type it schedules a delayed job for is actually a course association whose klass is User. If the symbol doesn't name a known association (or names one not backed by User), it raises 'invalid association' before enqueueing the job.","triggerScenarios":"Calling course.clear_todo_list_cache_later(:some_symbol) where :some_symbol is not an association defined on Course, or is an association that isn't to User (e.g. an association to another model or nonexistent).","commonSituations":"Plugin/patch code adds a new admin-like association but passes the wrong symbol; renaming a Course association (e.g. :admins, :teachers, :students, :ta) without updating clear_todo_list_cache_later call sites; typo'd symbol in custom invalidation code.","solutions":["Pass one of the existing User associations on Course (e.g. :admins, :teachers, :students, :tas).","Define the missing has_many association on Course with class_name 'User' before calling.","Fix the symbol typo at the call site; check with course.respond_to?(association_type) and Course.association(association_type).klass first."],"exampleFix":"// before\ncourse.clear_todo_list_cache_later(:instructor)\n\n// after\ncourse.clear_todo_list_cache_later(:teachers)","handlingStrategy":"validation","validationCode":"ok = Course.new.association(sym).klass == User rescue false","typeGuard":"def user_association?(course, sym)\n  course.association(sym.to_sym).klass == User\nrescue ArgumentError\n  false\nend","tryCatchPattern":null,"preventionTips":["Restrict calls to a constant allowlist of association symbols","Add unit tests for every clear_todo_list_cache_later call site","Check association existence with respond_to?/association before calling"],"tags":["ruby-on-rails","associations","caching"],"backgroundTag":"invalid-argument-value","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"}