{"record":{"id":"e96fe62a1188e3cf","repo":"instructure/canvas-lms","slug":"resource-url-must-match-the-resource-handler-s-domain","errorCode":null,"errorMessage":"resource url must match the resource handler's domain","messagePattern":"resource url must match the resource handler's domain","errorType":"exception","errorClass":"InvalidDomain","httpStatus":null,"severity":"error","filePath":"app/controllers/lti/message_controller.rb","lineNumber":153,"sourceCode":"    end\n\n    def assignment\n      @_assignment ||= if params[:assignment_id].present?\n                         @context.try(:active_assignments)&.find(params[:assignment_id])\n                       elsif params[:module_item_id].present?\n                         tag = ContentTag.not_deleted.find_by(id: params[:module_item_id])\n                         (tag&.context_type == \"Assignment\" && tag.context.context == @context) ? tag.context : nil\n                       elsif params[:secure_params].present?\n                         assignment = Assignment.from_secure_lti_params(params[:secure_params])\n                         (assignment&.root_account == @context.root_account) ? assignment : nil\n                       end\n    end\n\n    def launch_url(resource_url, message_handler)\n      if resource_url.present?\n        return resource_url if message_handler.valid_resource_url?(resource_url)\n\n        raise InvalidDomain, I18n.t(\"resource url must match the resource handler's domain\")\n      end\n      message_handler.launch_path\n    end\n\n    def basic_launch_by_lti_link(lti_link)\n      message_handler = lti_link.message_handler(@context)\n      if message_handler.present?\n        return lti2_basic_launch(message_handler, lti_link)\n      end\n\n      not_found\n    rescue InvalidDomain => e\n      render json: { errors: { invalid_launch_url: { message: e.message } } }, status: :bad_request\n    end\n\n    def lti2_basic_launch(message_handler, lti_link = nil)\n      resource_handler = message_handler.resource_handler\n      tool_proxy = resource_handler.tool_proxy","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/controllers/lti/message_controller.rb#L135-L171","documentation":"Raised by Lti::MessageController#launch_url when a LaunchTool link specifies an explicit resource_url that does not pass MessageHandler#valid_resource_url? — i.e. the URL's host is not a domain registered on the tool's message handler. Canvas rejects the LTI launch rather than redirecting users to an unregistered domain.","triggerScenarios":"POSTing to an LTI 1.x/2.x launch endpoint with a resource_url parameter whose domain differs from the message_handler's configured launch domain; tool configuration changed so the registered domain no longer matches the deep-link URL being launched.","commonSituations":"Admins migrate a tool to a new domain but old course navigation/deep-link URLs still point at the old host; self-hosted tools behind changing hostnames; copy-pasting launch URLs between environments (beta vs production).","solutions":["Fix the resource_url in the launch link/tool placement so its domain matches the message handler's registered domain","Update the tool's message_handler configuration (valid_resource_url domains) to include the new host","Remove the resource_url override so the handler's default launch_path is used","Re-create the affected placements/links with the correct domain"],"exampleFix":"// before\n<a href=\"/courses/1/external_tools/5?resource_url=https://old-tool.example.com/lti/launch\">Tool</a>\n// after\n<a href=\"/courses/1/external_tools/5?resource_url=https://tool.example.com/lti/launch\">Tool</a>","handlingStrategy":"validation","validationCode":"raise unless message_handler.valid_resource_url?(resource_url) || resource_url.blank?","typeGuard":"def valid_resource_url?(url) = URI.parse(url).host.in?(allowed_handler_domains)","tryCatchPattern":"begin\n  launch_url(resource_url, message_handler)\nrescue Lti::MessageController::InvalidDomain => e\n  Rails.logger.warn(\"launch blocked: #{e.message}\"); render json: { error: 'resource_url domain not allowed' }, status: :bad_request\nend","preventionTips":["Regenerate placement links after tool domain changes","Keep handler valid domains in sync with DNS","Prefer omitting resource_url to use launch_path"],"tags":["lti","ruby","url-validation"],"backgroundTag":"invalid-url","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}