jnunemaker/httparty · error · HTTParty::UnsafeURIError

Requested URI '#{new_uri}' has host '#{new_uri.host}' but th

Error message

Requested URI '#{new_uri}' has host '#{new_uri.host}' but the configured base_uri '#{normalized_base}' has host '#{normalized_base.host}'. This request could send credentials to an unintended server.

What it means

HTTParty raises UnsafeURIError from validate_uri_safety! when a request uses an absolute URI whose host differs from the configured base_uri host. This is an SSRF guard: if a class pins base_uri (typically alongside basic_auth/digest_auth or headers with secrets), a full URL pointing at another host would silently send those credentials elsewhere, so httparty aborts. The check is skipped for redirects and when options[:skip_uri_validation] is set, and only runs when options[:base_uri] is present.

Source

Thrown at lib/httparty/request.rb:460

        text,
        content_type: content_type,
        assume_utf16_is_big_endian: assume_utf16_is_big_endian
      ).call
    end

    def validate_uri_safety!(new_uri)
      return if options[:skip_uri_validation]

      configured_base_uri = options[:base_uri]
      return unless configured_base_uri

      normalized_base = options[:uri_adapter].parse(
        HTTParty.normalize_base_uri(configured_base_uri)
      )

      return if new_uri.host == normalized_base.host

      raise UnsafeURIError,
        "Requested URI '#{new_uri}' has host '#{new_uri.host}' but the " \
        "configured base_uri '#{normalized_base}' has host '#{normalized_base.host}'. " \
        "This request could send credentials to an unintended server."
    end
  end
end

View on GitHub (pinned to 8f4a09e343)

Solutions

  1. Keep hosts consistent: request relative paths against the pinned base_uri (`Api.get('/resource')`).
  2. If the different host is intentional and trusted, move that call to a separate HTTParty class with the correct base_uri (or none).
  3. Only for vetted URLs, bypass explicitly per request: `Api.get(url, skip_uri_validation: true)` — this re-enables credential leakage risk, so scope it narrowly.
  4. When consuming pagination 'next' links, parse them and re-issue relative paths instead of following absolute URLs.

Example fix

# before
class Api
  include HTTParty
  base_uri 'https://api.example.com'
  basic_auth 'user', 'pass'
end
Api.get('https://cdn.example.net/file')   # UnsafeURIError

# after (relative path against pinned host)
Api.get('/file')

# after (intentional different host: separate class, no shared creds)
class Cdn
  include HTTParty
end
Cdn.get('https://cdn.example.net/file')
Defensive patterns

Strategy: try-catch

Validate before calling

base = URI.parse(HTTParty.normalize_base_uri(base_uri_string))
raise HTTParty::UnsafeURIError, "#{uri.host} != pinned #{base.host}" if uri.host && uri.host != base.host

Type guard

host_matches_base = ->(url, base) do
  URI.parse(url.to_s).host == URI.parse(HTTParty.normalize_base_uri(base)).host
end

Try / catch

begin
  Api.get(url)
rescue HTTParty::UnsafeURIError => e
  Rails.logger.warn("host mismatch blocked: #{e.message}")
  OtherClient.get(url)  # separate class without shared credentials
end

Prevention

When it happens

Trigger: `class Api; include HTTParty; base_uri 'https://api.example.com'; basic_auth 'u','p'; end` then `Api.get('https://other-host.io/resource')`; or dynamic absolute URLs read from a field that no longer matches the pinned base host (e.g. environment mismatch where base_uri points at staging but the URL is production).

Common situations: API clients that pin base_uri for safety but sometimes receive absolute URLs (pagination next links, webhooks, entity hrefs), multi-environment configs where base_uri and the passed URL drift, and refactors that add base_uri to an existing class already passing full URLs.

Related errors


AI-assisted analysis of jnunemaker/httparty@8f4a09e343 (2026-08-21). Data as JSON: /api/errors/d1f91108ba1847f8. Report an issue: GitHub.