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

  1. Replace every raw & in URL values with & in the configuration XML
  2. Ask the tool vendor to regenerate the config with XML-escaped URLs
  3. 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&amp;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

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)