instructure/canvas-lms · error · UnresolvableUriError

# resolves to only unparseable IPs...

Error message

#{host} resolves to only unparseable IPs...

What it means

During blocklist validation, CanvasHttp converts each resolved address into an IPAddr. Addresses that fail to parse are logged as warnings and dropped; if nothing parseable remains, the host's IPs cannot be checked against the blocked ranges, so UnresolvableUriError is raised. This is a defensive guard against an 'impossible' Resolv behavior.

Solutions

  1. Inspect logs for the 'CANVAS_HTTP WARNING | invalid_ip' lines to see what unparseable value is returned
  2. Fix or replace the misbehaving DNS resolver configuration
  3. Upgrade/verify the Ruby Resolv library behavior in the environment
  4. As a workaround, rescue UnresolvableUriError and re-fetch after resolver fixes

Example fix

// before
CanvasHttp.get('https://weird-host.example.com')
// after
begin
  CanvasHttp.get('https://weird-host.example.com')
rescue CanvasHttp::UnresolvableUriError => e
  Rails.logger.warn("host validation failed: #{e.message}")
end
Defensive patterns

Strategy: try-catch

Try / catch

begin
  CanvasHttp.get(url)
rescue CanvasHttp::UnresolvableUriError => e
  logger.warn("unparseable IPs for host: #{e.message}")
  nil
end

Prevention

When it happens

Trigger: CanvasHttp request helpers where Resolv.getaddresses returns entries that IPAddr.new cannot parse (corrupt resolver data, weird resolver output) so the filtered ip_addrs list ends up empty while blocked_ip_ranges is non-empty.

Common situations: Custom or broken DNS resolvers returning malformed strings; proxies/resolv.conf setups injecting unexpected values; running with an unusual Ruby/Resolv combination.

Related errors


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

Appendix: source

Thrown at gems/canvas_http/lib/canvas_http.rb:287

    return false if blocked_ip_ranges.empty?

    resolved_addrs = Resolv.getaddresses(host)
    unless resolved_addrs.any?
      # this is actually a different condition than the host being insecure,
      # and having separate telemetry is helpful for understanding transient failures.
      raise UnresolvableUriError, "#{host} cannot be resolved to any address"
    end

    ip_addrs = resolved_addrs.filter_map do |ip|
      ::IPAddr.new(ip)
    rescue IPAddr::InvalidAddressError
      # this should never happen, Resolv should only be passing back IPs, but
      # let's make sure we can see if the impossible occurs
      logger.warn("CANVAS_HTTP WARNING | host: #{host} | invalid_ip: #{ip}")
      nil
    end
    unless ip_addrs.any?
      raise UnresolvableUriError, "#{host} resolves to only unparseable IPs..."
    end

    blocked_ip_ranges.each do |range|
      addr_range = ::IPAddr.new(range)
      ip_addrs.any? do |addr|
        if addr_range.include?(addr)
          logger.warn("CANVAS_HTTP WARNING insecure address | host: #{host} | insecure_address: #{addr} | range: #{range}")
          return true
        end
      end
    end
    false
  end

  # returns a Net::HTTP connection object for the given URI object
  def self.connection_for_uri(uri)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = (uri.scheme == "https")

View on GitHub (pinned to 1c9f0bb801)