instructure/canvas-lms · error · CCImportError
Invalid url in xml. Ampersands must be escaped.
Error message
Invalid url in xml. Ampersands must be escaped.
What it means
check_for_unescaped_url scans URL property values in the imported BLTI config for query strings whose parameters are separated by a bare, unescaped ampersand. Because the surrounding XML would need &, a raw '&' inside a URL signals malformed/unsafe markup, so a CCImportError is raised.
Solutions
- Replace every raw & in URL values with & in the configuration XML
- Ask the tool vendor to regenerate the config with XML-escaped URLs
- If generating XML programmatically, use a builder (Nokogiri::XML::Builder) so escaping is automatic
Example fix
# before <blti:launch_url>https://tool.example.com/launch?course_id=1&user_id=2</blti:launch_url> # after <blti:launch_url>https://tool.example.com/launch?course_id=1&user_id=2</blti:launch_url>
Defensive patterns
Strategy: validation
Validate before calling
raise CCImportError, 'unescaped ampersand in URL' if url.match?(/\?[^"<]*&/)
Type guard
def urls_escaped?(doc)
doc.xpath('//*[contains(local-name(), "url")]').all? do |n|
!n.text.match?(/(.*[^=]*\?*=)[^&;]*=/)
end
end Try / catch
begin
tool = converter.convert_blti_xml(xml)
rescue CCImportError => e
raise unless e.message.include?('Ampersands must be escaped')
Rails.logger.warn('Config XML contains unescaped ampersand in URL')
end Prevention
- Always write & instead of & in URL values inside XML
- Generate config XML with Nokogiri::XML::Builder so escaping is automatic
- Pre-validate vendor-supplied XML with the same regex before import
When it happens
Trigger: check_for_unescaped_url_properties walks the parsed tool config and finds a property/element named 'url' (or similar) whose value matches /(.*[^=]*\?*=)[^&;]*=/ — i.e. a URL with a query string containing an unescaped ampersand parameter separator.
Common situations: Vendors hand-generate config XML with raw '&' in launch/custom URLs; a config copied from a browser address bar keeps literal ampersands; template rendering did not HTML-escape the URL.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/67875b44f2182111.
Report an issue: GitHub.
Appendix: source
Thrown at lib/cc/importer/blti_converter.rb:139
end
tool
end
def check_for_unescaped_url_properties(obj)
# Recursively look for properties named 'url'
case obj
when Hash
obj.select { |k, v| k.to_s == "url" && v.is_a?(String) }
.each_value { |v| check_for_unescaped_url(v) }
obj.each_value { |v| check_for_unescaped_url_properties(v) }
when Array
obj.each { |o| check_for_unescaped_url_properties(o) }
end
end
def check_for_unescaped_url(url)
if /(.*[^=]*\?*=)[^&;]*=/.match?(url)
raise CCImportError, I18n.t(:invalid_url_in_xml, "Invalid url in xml. Ampersands must be escaped.")
end
end
def retrieve_and_convert_blti_url(url)
InstrumentTLSCiphers.without_tls_metrics do
response = CanvasHttp.get(url, redirect_limit: 10)
config_xml = response.body
convert_blti_xml(config_xml)
rescue Timeout::Error
raise CCImportError, I18n.t(:retrieve_timeout, "could not retrieve configuration, the server response timed out")
end
end
def get_custom_properties(node)
props = {}
node.children.each do |property|
next if property.name == "text"
View on GitHub (pinned to 1c9f0bb801)