instructure/canvas-lms · error · InvalidDomain

resource url must match the resource handler's domain

Error message

resource url must match the resource handler's domain

What it means

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.

Solutions

  1. Fix the resource_url in the launch link/tool placement so its domain matches the message handler's registered domain
  2. Update the tool's message_handler configuration (valid_resource_url domains) to include the new host
  3. Remove the resource_url override so the handler's default launch_path is used
  4. Re-create the affected placements/links with the correct domain

Example fix

// before
<a href="/courses/1/external_tools/5?resource_url=https://old-tool.example.com/lti/launch">Tool</a>
// after
<a href="/courses/1/external_tools/5?resource_url=https://tool.example.com/lti/launch">Tool</a>
Defensive patterns

Strategy: validation

Validate before calling

raise unless message_handler.valid_resource_url?(resource_url) || resource_url.blank?

Type guard

def valid_resource_url?(url) = URI.parse(url).host.in?(allowed_handler_domains)

Try / catch

begin
  launch_url(resource_url, message_handler)
rescue Lti::MessageController::InvalidDomain => e
  Rails.logger.warn("launch blocked: #{e.message}"); render json: { error: 'resource_url domain not allowed' }, status: :bad_request
end

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


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

Appendix: source

Thrown at app/controllers/lti/message_controller.rb:153

    end

    def assignment
      @_assignment ||= if params[:assignment_id].present?
                         @context.try(:active_assignments)&.find(params[:assignment_id])
                       elsif params[:module_item_id].present?
                         tag = ContentTag.not_deleted.find_by(id: params[:module_item_id])
                         (tag&.context_type == "Assignment" && tag.context.context == @context) ? tag.context : nil
                       elsif params[:secure_params].present?
                         assignment = Assignment.from_secure_lti_params(params[:secure_params])
                         (assignment&.root_account == @context.root_account) ? assignment : nil
                       end
    end

    def launch_url(resource_url, message_handler)
      if resource_url.present?
        return resource_url if message_handler.valid_resource_url?(resource_url)

        raise InvalidDomain, I18n.t("resource url must match the resource handler's domain")
      end
      message_handler.launch_path
    end

    def basic_launch_by_lti_link(lti_link)
      message_handler = lti_link.message_handler(@context)
      if message_handler.present?
        return lti2_basic_launch(message_handler, lti_link)
      end

      not_found
    rescue InvalidDomain => e
      render json: { errors: { invalid_launch_url: { message: e.message } } }, status: :bad_request
    end

    def lti2_basic_launch(message_handler, lti_link = nil)
      resource_handler = message_handler.resource_handler
      tool_proxy = resource_handler.tool_proxy

View on GitHub (pinned to 1c9f0bb801)