instructure/canvas-lms · error · UnresolvableUriError

# cannot be resolved to any address

Error message

#{host} cannot be resolved to any address

What it means

CanvasHttp's host-blocklist check resolves the hostname via Resolv.getaddresses before comparing against blocked IP ranges. If DNS returns no addresses at all, the host cannot be validated, so instead of silently proceeding it raises UnresolvableUriError. This prevents SSRF protection from being bypassed by unresolvable hosts.

Solutions

  1. Verify the hostname with `dig`/`nslookup` or Resolv.getaddresses(host) and fix the typo if the domain is wrong
  2. Fix DNS configuration (resolv.conf, container network, VPC DNS) so the host resolves
  3. If the host is internal, add it to DNS or /etc/hosts on the app servers, then retry
  4. Rescue UnresolvableUriError in the caller and treat it as a retryable/transient fetch failure

Example fix

// before
CanvasHttp.get('https://servcie.internal.example.com/api')
// after
host = 'service.internal.example.com'
raise ArgumentError, 'unresolvable host' if Resolv.getaddresses(host).empty?
CanvasHttp.get("https://#{host}/api")
Defensive patterns

Strategy: retry

Validate before calling

resolved = Resolv.getaddresses(host)
raise ArgumentError, 'host does not resolve' if resolved.empty?

Type guard

def resolvable?(host) = Resolv.getaddresses(host).any?

Try / catch

begin
  CanvasHttp.get(url)
rescue CanvasHttp::UnresolvableUriError => e
  logger.warn("DNS failure for #{e.message}")
  retry_later(url)
end

Prevention

When it happens

Trigger: CanvasHttp.get/post (or other request helpers) called with a URL whose host fails DNS resolution: nonexistent domain, DNS outage, /etc/hosts-only names, or typoed internal hostnames when blocked_ip_ranges is non-empty.

Common situations: Fetching webhook/target URLs with stale or misconfigured DNS; container environments lacking DNS; typos in configured service hostnames; intermittent DNS transient failures during fetch jobs.

Related errors


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

Appendix: source

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

        value = "#{scheme}://#{value}"
      end
      uri = URI.parse(value) # it's still a URI::Generic
    end
    raise ArgumentError if !allowed_schemes.nil? && !allowed_schemes.include?(uri.scheme.downcase)
    raise(RelativeUriError) if uri.host.nil? || uri.host.strip.empty?
    raise InsecureUriError if check_host && insecure_host?(uri.host)

    [value, uri]
  end

  def self.insecure_host?(host)
    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)

View on GitHub (pinned to 1c9f0bb801)