instructure/canvas-lms · error · ImportError
An email did not pass validation
Error message
An email did not pass validation (#{user_row.email}, error: #{cc.errors.full_messages.join(", ")}) What it means
SisImports::ImportError raised in SIS::UserImporter#process_batch when the CommunicationChannel created for the user's email address fails validation (cc.save! path fails / cc invalid). All of cc.errors.full_messages are joined into the message together with the offending email value.
Solutions
- Fix the email value reported in the error message so it is a syntactically valid address
- Trim whitespace and remove illegal characters (spaces, multiple @, unescaped commas) from the email column
- If email is optional for this user, remove the value rather than sending an invalid one (subject to account policy)
- Add CSV-level email format pre-validation before upload
Example fix
// before (users.csv) user_id,email 1001,jane doe@school edu // after user_id,email 1001,jane.doe@school.edu
Defensive patterns
Strategy: validation
Validate before calling
EMAIL_RE = URI::MailTo::EMAIL_REGEXP
rows.each do |r|
next if r[:email].blank?
raise "invalid email #{r[:email]}" unless EMAIL_RE.match?(r[:email].strip)
end Type guard
def valid_email?(v) = v.is_a?(String) && URI::MailTo::EMAIL_REGEXP.match?(v.strip)
Try / catch
begin
importer.add_user(row)
rescue SisImports::ImportError => e
raise unless e.message.start_with?('An email did not pass validation')
logger.warn("Skipping row with bad email: #{e.message}")
end Prevention
- Validate the email column with a regex before uploading the SIS batch
- Trim whitespace and reject values containing spaces or commas
- Keep emails optional-empty rather than invalid-filled when unknown
When it happens
Trigger: During process_batch (called from add_user), the row supplies an email column and the importer builds a CommunicationChannel; when the channel is invalid (e.g. malformed or blank email address per EmailValidator / format rules), the else branch raises ImportError with the email and the full error list.
Common situations: Emails with stray spaces, commas, or missing TLDs in users.csv; empty email strings for users where email is required; case or plus-addressing variants tripping stricter validators; data migrated from another SIS with loosely validated addresses.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- A user did not pass validation
- An institutional tag category did not pass validation
- Cannot find # for ID: #
- Context is required, but none found
- Context must be of type '#
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/79f707bd9e09c84a.
Report an issue: GitHub.
Appendix: source
Thrown at lib/sis/user_importer.rb:431
sis_cc = nil
end
cc = sis_cc || other_cc || user.communication_channels.new
cc.user_id = user.id
cc.pseudonym_id = pseudo.id
cc.path = user_row.email
cc.bounce_count = 0 if cc.path_changed?
cc.workflow_state = (status == "deleted") ? "retired" : "active"
newly_active = cc.path_changed? || (cc.active? && cc.workflow_state_changed?)
if cc.changed?
if cc.valid? && cc.save_without_broadcasting
record_sync_for(user)
cc_data = SisBatchRollBackData.build_data(sis_batch: @batch, context: cc)
@roll_back_data << cc_data if cc_data
else
msg = "An email did not pass validation "
msg += "(" + "#{user_row.email}, error: "
msg += cc.errors.full_messages.join(", ") + ")"
raise ImportError, msg
end
user.touch unless user_touched
user.clear_email_cache!
end
pseudo.sis_communication_channel_id = pseudo.communication_channel_id = cc.id
if newly_active && @root_account.feature_enabled?(:self_service_user_merge)
user_ids = ccs.map(&:user_id)
pseudo_scope = Pseudonym.active.where(user_id: user_ids).group(:user_id)
active_pseudo_counts = pseudo_scope.count
sis_pseudo_counts = pseudo_scope.where("account_id = ? AND sis_user_id IS NOT NULL", @root_account).count
other_ccs = ccs.reject do |other|
cc_user_id = other.user_id
same_user = cc_user_id == user.id
no_active_pseudos = active_pseudo_counts.fetch(cc_user_id, 0) == 0
active_sis_pseudos = sis_pseudo_counts.fetch(cc_user_id, 0) != 0
View on GitHub (pinned to 1c9f0bb801)