instructure/canvas-lms · error · BasicLTI::Errors::InvalidSourceId
user_not_in_course
user_not_in_course
Error message
User is no longer in course
What it means
BasicLti::Sourcedid#validate! re-resolves the LTI launch context from the sourcedid token. It raises Errors::InvalidSourceId with code :user_not_in_course when the user lookup returns nil, meaning the enrollment that originally authorized this launch no longer exists or is not active. The library throws it to prevent further LTI API operations (grades, returns) on behalf of a user who can no longer access the course.
Solutions
- Re-launch the tool to obtain a fresh sourcedid for the current enrollment.
- Check the user's enrollment state in the course (Enrollment.active.where(user_id:, course_id:)) before sending grades with the old sourcedid.
- If the user was removed intentionally, stop using the stored sourcedid and drop it from your sync queue.
- If this is unexpected, verify the user_id in the sourcedid token matches the intended student and that no enrollment cleanup job deleted it.
Example fix
// before
sub = BasicLti::Sourcedid.load!(sourcedid) # raises if student unenrolled
sub.update_submission_score(score)
// after
begin
sub = BasicLti::Sourcedid.load!(sourcedid)
sub.update_submission_score(score)
rescue BasicLti::Sourcedid::Errors::InvalidSourceId => e
Rails.logger.warn("skipping stale sourcedid: #{e.message}") if e.error_code == :user_not_in_course
end Defensive patterns
Strategy: validation
Validate before calling
# ruby valid = Enrollment.active.where(user_id: user_id, course_id: course_id).exists? raise "stale sourcedid: user not enrolled" unless valid
Try / catch
begin BasicLti::Sourcedid.load!(sourcedid) rescue BasicLti::Sourcedid::Errors::InvalidSourceId => e handle_stale_sourcedid if e.error_code == :user_not_in_course end
Prevention
- Listen for enrollment-deleted/concluded events and invalidate stored sourcedids.
- Re-fetch sourcedids via fresh launches instead of long-term storage.
- Skip-and-log stale sourcedids in grade sync jobs instead of failing the whole batch.
When it happens
Trigger: Calling Sourcedid.load!(sourcedid) (which runs validate!) when token[:user_id] does not resolve to a user with an active enrollment in the course — e.g. the student was unenrolled or the enrollment was concluded/deleted after the launch.
Common situations: A student dropped the course and the tool later calls grade passback or a course-navigation return using the stored sourcedid; batch grade synchronization with stale lis_result_sourcedid values; a course copy/cleanup that removed enrollments.
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
- Access token expired
- Access token invalid - signature likely incorrect
- assignment_invalid
- Assignment not configured for external tool launches
- Assignment not configured for launches with specified tool
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/6e79820ae687a064.
Report an issue: GitHub.
Appendix: source
Thrown at lib/basic_lti/sourcedid.rb:55
Canvas::Security.base64_encode(crypted_token)
end
def jwt_payload
{
iss: "Canvas",
aud: ["Instructure"],
iat: Time.zone.now.to_i,
tool_id: tool.id,
course_id: course.id,
assignment_id: assignment.id,
user_id: user.id,
}
end
private :jwt_payload
def validate!
raise Errors::InvalidSourceId.new("Course is invalid", :course_invalid) unless course
raise Errors::InvalidSourceId.new("User is no longer in course", :user_not_in_course) unless user
raise Errors::InvalidSourceId.new("Assignment is invalid", :assignment_invalid) unless assignment
tag = assignment.external_tool_tag
raise Errors::InvalidSourceId.new("Assignment is no longer associated with this tool", :assignment_tool_mismatch) unless tag &&
(tool.matches_url?(tag.url, match_queries_exactly: false) || tool.matches_tool_domain?(tag.url)) &&
tool.workflow_state != "deleted"
end
def self.load!(sourcedid_string)
raise Errors::InvalidSourceId.new("Invalid sourcedid", :sourcedid_invalid) if sourcedid_string.blank?
token = load_from_legacy_sourcedid!(sourcedid_string) ||
token_from_sourcedid!(sourcedid_string)
tool = Lti::ToolFinder.find_by(id: token[:tool_id])
course = Course.active.find_by(id: token[:course_id])
if course
user = course.student_enrollments.active.find_by(user_id: token[:user_id])&.userView on GitHub (pinned to 1c9f0bb801)