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
- Verify the hostname with `dig`/`nslookup` or Resolv.getaddresses(host) and fix the typo if the domain is wrong
- Fix DNS configuration (resolv.conf, container network, VPC DNS) so the host resolves
- If the host is internal, add it to DNS or /etc/hosts on the app servers, then retry
- 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
- Monitor DNS health in app containers/hosts
- Validate configured hostnames at boot
- Use fully-qualified domain names for internal services
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
- # resolves to only unparseable IPs...
- CANVAS_HTTP WARNING | host: #
- Consul is unavailable because the circuit breaker has…
- could not retrieve configuration, the server response timed…
- Couldn't send LTI AGS grade progress metric
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)