instructure/canvas-lms · error · ImportError

Invalid type '# ' for change_sis_id

Error message

Invalid type '#{type}' for change_sis_id

What it means

After downcasing/stripping the type, process_change_sis_id looks it up in a fixed hash of seven supported types (user, course, section, term, account, group, group_category). Any other value raises this ImportError because there is no scope/column mapping for it.

Solutions

  1. Use exactly one of: user, course, section, term, account, group, group_category (case/whitespace is normalized).
  2. To change a user's SIS login id note 'user' maps to Pseudonyms with column sis_user_id.
  3. Reject unsupported types at CSV-generation time in the upstream system.

Example fix

// before
type,old_id,new_id
enrollment,E1,E2
// after
type,old_id,new_id
course,C1,C2
Defensive patterns

Strategy: validation

Validate before calling

VALID_TYPES = %w[user course section term account group group_category]
unless VALID_TYPES.include?(dc.type.to_s.downcase.strip)
  raise ArgumentError, "unsupported type: #{dc.type}"
end
process_change_sis_id(dc)

Type guard

VALID_TYPES = %w[user course section term account group group_category].freeze
def supported_type?(dc)
  VALID_TYPES.include?(dc.type.to_s.downcase.strip)
end

Try / catch

begin
  process_change_sis_id(dc)
rescue SIS::ImportError => e
  errors << e.message if e.message.start_with?('Invalid type')
end

Prevention

When it happens

Trigger: type values like 'enrollment', 'assignment', 'User ' variants that map to nothing after downcase/strip (e.g. 'pseudonym', 'users', 'group_category ' misspelled as 'groupcategory'), or singular/plural mismatches.

Common situations: Assuming any Canvas object type is changeable; typos in hand-written CSVs; integrations emitting their own internal type names instead of Canvas's change_sis_id type vocabulary.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at lib/sis/change_sis_id_importer.rb:86

        raise ImportError, "No type given for change_sis_id" if data_change.type.blank?
        raise ImportError, "No old_id or old_integration_id given for change_sis_id" if data_change.old_id.blank? && data_change.old_integration_id.blank?
        raise ImportError, "No new_id or new_integration_id given for change_sis_id" if data_change.new_id.blank? && data_change.new_integration_id.blank?

        type = data_change.type.downcase.strip
        things_to_update_batch_ids[type] ||= Set.new

        types = {
          "user" => { scope: @root_account.pseudonyms, column: :sis_user_id },
          "course" => { scope: @root_account.all_courses },
          "section" => { scope: @root_account.course_sections },
          "term" => { scope: @root_account.enrollment_terms },
          "account" => { scope: @root_account.all_accounts },
          "group" => { scope: @root_account.all_groups },
          "group_category" => { scope: @root_account.all_group_categories },
        }

        details = types[type]
        raise ImportError, "Invalid type '#{type}' for change_sis_id" unless details

        column = details[:column] || :sis_source_id
        check_for_conflicting_ids(column, details, type, data_change)
        old_item = find_item_to_update(column, details, type, data_change)
        update_record(column, data_change, details, old_item)
        things_to_update_batch_ids[type] << old_item.id
        users_to_sync << old_item.user_id if type == "user"
        self.success_count += 1
      end

      def update_record(column, data_change, details, old_item)
        updates = ids_to_change(column, data_change)
        details[:scope].where(id: old_item.id).update_all(updates)
        old_item.invalidate_association_cache if old_item.is_a?(Account)
        # update_all bypasses AR callbacks; re-index GlobalLookups
        # so cross-shard searches can find the new IDs.
        if old_item.is_a?(Pseudonym) && GlobalLookups.enabled?
          old_item.delay_if_production.ensure_global_lookup_record(force: true)

View on GitHub (pinned to 1c9f0bb801)