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
- 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
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
- Regenerate placement links after tool domain changes
- Keep handler valid domains in sync with DNS
- Prefer omitting resource_url to use launch_path
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
- assets_url for '# ' must be a valid URL
- Assignment not configured for external tool launches
- Developer key mismatch
- e.message
- Missing required asset parameter #
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_proxyView on GitHub (pinned to 1c9f0bb801)