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

tool_invalid

tool_invalid

Error message

Tool is invalid

What it means

BasicLTI::BasicOutcomes.decode_source_id loads the signed sourcedid and compares its embedded tool with the tool the request was authenticated as. If they differ, the request is from a different tool than the one that issued the sourcedid, so InvalidSourceId with code tool_invalid is raised.

Solutions

  1. Ensure the tool authenticating the outcomes request is the same one that generated the sourcedid (same consumer key)
  2. Re-issue the LTI launch so the student gets a fresh sourcedid bound to the current tool
  3. Check for duplicated or re-keyed ContextExternalTool records and consolidate them
  4. Verify the TP is not replaying sourcedids captured from another tool's launches

Example fix

// before
tool = ContextExternalTool.find_by(consumer_key: shared_key) // may differ from sourcedid tool
// after
tool = BasicLTI::Sourcedid.load!(sourceid).tool // use the sourcedid's own tool
Defensive patterns

Strategy: try-catch

Validate before calling

sourcedid = BasicLTI::Sourcedid.load!(sourceid)
raise "tool mismatch" unless sourcedid.tool.id == tool.id

Try / catch

begin
  assignment, user = BasicLTI::BasicOutcomes.decode_source_id(tool, sourceid)
rescue BasicLTI::Errors::InvalidSourceId => e
  return xml failure response for code e.code
end

Prevention

When it happens

Trigger: A Basic Outcomes (grade replace) XML request authenticated with a different OAuth consumer key/tool than the one encoded in the lis_result_sourcedid; a tool re-keyed or recreated so its id no longer matches; sourcedid copied between tool contexts.

Common situations: Shared consumer keys across multiple tool deployments; a tool re-registered with a new key mid-flight so old sourcedids validate against the new tool object; TP forwarding grade requests from another tool's launch.

Related errors


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

Appendix: source

Thrown at lib/basic_lti/basic_outcomes.rb:76

        InstStatsd::Statsd.distributed_increment("lti.1_1.basic_outcomes.bad_requests",
                                                 tags: { error_code: "InvalidRequest" })
        super
      end

      def response_status
        415
      end
    end

    # gives instfs about 7 hours to have an outage and eventually take the file
    MAX_ATTEMPTS = 10

    SOURCE_ID_REGEX = /^(\d+)-(\d+)-(\d+)-(\d+)-(\w+)$/

    def self.decode_source_id(tool, sourceid)
      tool.shard.activate do
        sourcedid = BasicLTI::Sourcedid.load!(sourceid)
        raise BasicLTI::Errors::InvalidSourceId.new("Tool is invalid", :tool_invalid) unless tool == sourcedid.tool

        return sourcedid.assignment, sourcedid.user
      end
    end

    def self.process_request(tool, xml)
      InstStatsd::Statsd.time("lti.1_1.basic_outcomes.process_request_time") do
        res = (quizzes_next_tool?(tool) ? BasicLTI::QuizzesNextLtiResponse : LtiResponse).new(xml)

        unless res.handle_request(tool)
          res.code_major = "unsupported"
          res.description = "Request could not be handled. ¯\\_(ツ)_/¯"
        end
        res
      end
    end

    def self.quizzes_next_tool?(tool)

View on GitHub (pinned to 1c9f0bb801)