instructure/canvas-lms · warning

CANVAS_HTTP WARNING | host: #

Error message

CANVAS_HTTP WARNING | host: #{host} | invalid_ip: #{ip}

What it means

CanvasHttp's SSRF protection resolves the host and parses each resolved address into IPAddr objects. If Resolv ever returns a value that isn't a parseable IP, this warning is logged per-address and the entry is dropped. If no addresses remain parseable, an UnresolvableUriError is raised. The warning itself means the 'impossible' case occurred.

Solutions

  1. Inspect what Resolv returns for the host (`Resolv.getaddresses host`) and fix the resolver/DNS config
  2. Strip IPv6 zone identifiers before validation if hosts contain %scope suffixes
  3. Update the parsing to normalize addresses (e.g., strip brackets from bracketed IPv6 literals) if applicable
  4. If only the warning fires but some IPs parse, behavior is safe — but investigate why Resolv emitted a non-IP

Example fix

// before
ip = 'fe80::1%eth0'
IPAddr.new(ip) # InvalidAddressError -> warning
// after
ip = ip.split('%').first
IPAddr.new(ip) # parses link-local address
Defensive patterns

Strategy: validation

Validate before calling

def parseable_ip?(addr)
  IPAddr.new(addr.to_s.sub(/%.*$/, ''))
  true
rescue IPAddr::InvalidAddressError
  false
end

Type guard

def valid_ip?(str)
  IPAddr.new(str.to_s)
  true
rescue IPAddr::InvalidAddressError
  false
end

Try / catch

begin
  CanvasHttp.get(url)
rescue CanvasHttp::UnresolvableUriError => e
  logger.error("host unresolvable/invalid: #{e.message}")
end

Prevention

When it happens

Trigger: resolved_addrs contains a string that IPAddr.new cannot parse (IPAddr::InvalidAddressError) while validating host before an outbound fetch — typically due to odd resolver output, IPv6 zone IDs, or custom DNS returning non-IP strings.

Common situations: Hostnames resolving through unusual resolver configurations; IPv6 literals with scope/zone identifiers (fe80::1%eth0); mocking/stubbing Resolv in tests with fake address strings; hosts-file entries with malformed values.

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/3d8b542644ba608c. Report an issue: GitHub.

Appendix: source

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

    [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)
          logger.warn("CANVAS_HTTP WARNING insecure address | host: #{host} | insecure_address: #{addr} | range: #{range}")
          return true
        end
      end
    end
    false
  end

View on GitHub (pinned to 1c9f0bb801)