instructure/canvas-lms · error · BasicLTI::Errors::InvalidSourceId
course_invalid
course_invalid
Error message
Course is invalid
What it means
BasicLTI::Sourcedid#validate! re-checks at load time that the course, user, and assignment referenced by the sourcedid still exist and match the tool. The first check raises InvalidSourceId with code course_invalid when the course record encoded in the sourcedid's JWT can no longer be found (e.g. deleted or on an inaccessible shard).
Solutions
- Re-launch the assignment so a fresh sourcedid referencing an existing course is issued
- Confirm the course still exists (Course.where(id: ...)) before the TP submits grades
- Check whether the course was soft-deleted (workflow_state) and restore it if needed
- Rescue BasicLTI::Errors::InvalidSourceId in the outcomes endpoint and return an appropriate failure to the TP
Example fix
// before
BasicLTI::BasicOutcomes.process_request(tool, xml) // raises if course deleted
// after
begin
BasicLTI::BasicOutcomes.process_request(tool, xml)
rescue BasicLTI::Errors::InvalidSourceId => e
logger.warn("sourcedid invalid: #{e.message}")
return failure response
end Defensive patterns
Strategy: try-catch
Validate before calling
payload = BasicLTI::Sourcedid.jwt_payload_for(course, user, assignment) raise "course missing" unless Course.exists?(payload[:course_id])
Try / catch
begin BasicLTI::BasicOutcomes.process_request(tool, xml) rescue BasicLTI::Errors::InvalidSourceId => e return failure xml with e.code (e.g. course_invalid) end
Prevention
- Re-launch LTI assignments after course changes
- Check course existence before delayed grade submissions
- Restore soft-deleted courses when delayed passbacks are expected
- Handle per-code failures distinctly in the TP response
When it happens
Trigger: A grade-passback request arrives with a sourcedid whose course was deleted after launch; course_id in the JWT not found in the database; cross-shard sourcedid loaded without the course present on the target shard.
Common situations: Courses deleted/concluded between LTI launch and grade submission; TP delaying grade submissions past course deletion; test-environment data not matching production sourcedids.
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
- tool_invalid
- A course with that id does not exist
- Access token expired
- Access token invalid - signature likely incorrect
- assignment_invalid
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/427a057cc0fe192e.
Report an issue: GitHub.
Appendix: source
Thrown at lib/basic_lti/sourcedid.rb:54
)
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 courseView on GitHub (pinned to 1c9f0bb801)