Freika/dawarich · warning · UrlValidatable::BlockedUrlError

URL must include a host

Error message

URL must include a host

What it means

Raised as BlockedUrlError by validate_integration_url! when the URI parses and has an acceptable http/https scheme but uri.host is blank. This happens for URLs whose authority component is missing: 'https:///path', 'https://:8080/x', or scheme-only strings like 'https://' — the parser accepts them but there is no host to resolve or connect to.

Source

Thrown at app/services/concerns/url_validatable.rb:66

    IPAddr.new('127.0.0.0/8'),     # IPv4 loopback
    IPAddr.new('172.16.0.0/12'),   # RFC1918
    IPAddr.new('192.0.0.0/24'),    # IETF protocol assignments
    IPAddr.new('192.168.0.0/16'),  # RFC1918
    IPAddr.new('198.18.0.0/15'),   # benchmark
    IPAddr.new('::1/128'),         # IPv6 loopback
    IPAddr.new('fc00::/7')         # IPv6 ULA
  ].freeze

  private

  def validate_integration_url!(url)
    return if url.blank?

    uri = URI.parse(url)
    unless %w[http https].include?(uri.scheme)
      raise BlockedUrlError, I18n.t('services.concerns.url_validatable.invalid_scheme', scheme: uri.scheme)
    end
    raise BlockedUrlError, I18n.t('services.concerns.url_validatable.host_required') if uri.host.blank?

    # Cloud refuses URLs that embed credentials. Self-hosters legitimately
    # use http://user:pass@host — homelab Immich behind nginx basic-auth
    # is a real config we don't want to break.
    if uri.userinfo.present? && !DawarichSettings.self_hosted?
      raise BlockedUrlError, I18n.t('services.concerns.url_validatable.embedded_credentials')
    end

    ip = IPAddr.new(Resolv.getaddress(uri.host))
    if blocked_ranges.any? { |range| range.include?(ip) }
      raise BlockedUrlError, I18n.t('services.concerns.url_validatable.blocked_address')
    end
  rescue URI::InvalidURIError
    raise BlockedUrlError, I18n.t('services.concerns.url_validatable.invalid_format')
  rescue Resolv::ResolvError
    raise BlockedUrlError, I18n.t('services.concerns.url_validatable.unresolvable_host', host: uri.host)
  end

View on GitHub (pinned to 97fad417c5)

Solutions

  1. Re-enter the URL with a real hostname: 'https://immich.example.com:2283'.
  2. If the URL is assembled from parts, guard each part is non-empty before building, or fail the save in the form.
  3. Inspect URI.parse(url) in a console (scheme/host/port/path) to see where the host went.
  4. Make the host field required and validated client-side.

Example fix

# before
base = ENV['IMMICH_HOST'] # nil in this env
url = "https://#{base}:2283"     # => "https://:2283", host nil

# after
base = ENV.fetch('IMMICH_HOST') { raise 'IMMICH_HOST not set' }
url = "https://#{base}:2283"    # host present
Defensive patterns

Strategy: validation

Validate before calling

uri = URI.parse(url.to_s)
uri.host.present? || errors.add(:url, 'host missing')

Type guard

def url_with_host?(s)
  u = URI.parse(s.to_s)
  %w[http https].include?(u.scheme) && u.host.present?
rescue URI::InvalidURIError
  false
end

Try / catch

begin
  validate_integration_url!(url)
rescue BlockedUrlError => e
  errors.add(:url, e.message)
end

Prevention

When it happens

Trigger: Saving 'https://' plus only a path or port ('https:///api', 'https://:2283'), a URL built by string interpolation where the host variable was empty ("https://#{host}:2283" with host=''), or user input that is just the scheme.

Common situations: Settings forms where the host field is optional and left blank while the client auto-prepends https://, template/env interpolation producing empty hosts, copy-paste that drops the hostname, trailing-punctuation hosts that URI.parse pushes into the path.

Related errors


AI-assisted analysis of Freika/dawarich@97fad417c5 (2026-08-21). Data as JSON: /api/errors/e7aa718410304bf1. Report an issue: GitHub.