instructure/canvas-lms · error · BasicLTI::Errors::InvalidSourceId

sourcedid_invalid

sourcedid_invalid

Error message

Invalid sourcedid

What it means

Sourcedid.load! raises Errors::InvalidSourceId with :sourcedid_invalid when the sourcedid string is blank, or (via token_from_sourcedid!) when the encrypted JWT payload cannot be parsed as a valid format (JSON::JWT::InvalidFormat rescued and re-raised). It is the generic 'this lis_result_sourcedid is not a decodable token' error.

Solutions

  1. Check the sourcedid is present and unmodified (no truncation, no double URL-encoding) before calling load!.
  2. Re-launch the tool to obtain a fresh sourcedid.
  3. If legacy sourcedids are in play, confirm load_from_legacy_sourcedid! path applies (SOURCE_ID_REGEX match) — legacy format is tried first.
  4. Log the received value safely (length/prefix only) to detect truncation or escaping corruption.

Example fix

// before
sourcedid = BasicLti::Sourcedid.load!(params[:lis_result_sourcedid])
// after
sd = params[:lis_result_sourcedid].to_s
if sd.blank? || sd.length < 50
  return render_error("missing or malformed sourcedid")
end
sourcedid = BasicLti::Sourcedid.load!(sd)
Defensive patterns

Strategy: validation

Validate before calling

# ruby
sd = params[:lis_result_sourcedid].to_s
return error("missing sourcedid") if sd.blank?

Try / catch

begin
  BasicLti::Sourcedid.load!(sd)
rescue BasicLti::Sourcedid::Errors::InvalidSourceId => e
  render_bad_request("invalid sourcedid") if e.error_code == :sourcedid_invalid
end

Prevention

When it happens

Trigger: Calling Sourcedid.load!(nil), load!("") , or passing a string that is not base64-encoded encrypted JWT data (e.g. an HTML-escaped, truncated, or legacy-corrupted value) to token_from_sourcedid!.

Common situations: Tool stores lis_result_sourcedid in a column/cookie that truncated it; sending the sourcedid URL-encoded/unescaped incorrectly; passing an old OAuth-signed sourcedid through the JWT path after a format migration.

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


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

Appendix: source

Thrown at lib/basic_lti/sourcedid.rb:65

        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])&.user
        assignment = course.assignments.active.find_by(id: token[:assignment_id])
      end

      sourcedid = new(tool, course, assignment, user)
      sourcedid.validate!
      sourcedid
    end

    def self.load_from_legacy_sourcedid!(sourcedid)
      token = nil

View on GitHub (pinned to 1c9f0bb801)