Faraday Happy Eyeballs

A Faraday adapter that implements the Happy Eyeballs algorithm (RFC 6555) for intelligent IPv4/IPv6 dual-stack connections.

What is Happy Eyeballs?

Happy Eyeballs (RFC 6555) is an algorithm that makes dual-stack applications (IPv4 and IPv6) more responsive by racing IPv4 and IPv6 connection attempts. Instead of waiting for one protocol to timeout before trying the other, it attempts both simultaneously with intelligent timing to minimize connection delays.

Features

  • RFC 6555 Compliant: Implements the official Happy Eyeballs algorithm
  • Automatic Fallback: Seamlessly falls back between IPv6 and IPv4
  • Connection Caching: Caches successful connection preferences to avoid repeated resolution
  • Configurable Timing: Customize connection delays and timeouts
  • Thread Safety: Safe for concurrent use
  • Small Dependency Surface: Requires Faraday and Ruby standard library

Installation

Add this line to your application's Gemfile:

gem 'faraday-happy-eyeballs'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install faraday-happy-eyeballs

Usage

Basic Usage

require 'faraday'
require 'faraday/happy_eyeballs'

client = Faraday.new('https://example.com') do |faraday|
  faraday.adapter :happy_eyeballs
end

response = client.get('/')

Custom Configuration

client = Faraday.new('https://example.com') do |faraday|
  faraday.adapter :happy_eyeballs, {
    connection_attempt_delay: 0.25,  # Delay between connection attempts (seconds)
    resolution_delay: 0.05,          # Delay between DNS resolutions (seconds)
    first_address_family_count: 1,   # Number of preferred family addresses to try first
    connection_timeout: 10,          # Total timeout for all connection attempts
    cache_ttl: 300,                  # Cache TTL for successful connections (seconds)
    prefer_ipv6: true,               # Prefer IPv6 addresses (RFC recommendation)
    enable_cache: true,              # Enable connection result caching
    ca_file: '/path/to/custom-ca-bundle.pem' # Optional CA bundle for HTTPS
  }
end

# The CA file can also be supplied through Faraday's SSL options:
client = Faraday.new('https://example.com', ssl: {
  ca_file: '/path/to/custom-ca-bundle.pem'
}) do |faraday|
  faraday.adapter :happy_eyeballs
end

Stacking with Other Middleware

client = Faraday.new('https://example.com') do |faraday|
  faraday.use :retry, max: 3
  faraday.use :gzip
  faraday.adapter :happy_eyeballs, prefer_ipv6: false
end

Twilio Ruby Client

The Twilio constructor does not take Faraday SSL options. Configure the Twilio HTTP client before making a request:

twilio = Twilio::REST::Client.new(api_key_sid, api_secret, )

twilio.http_client.configure_connection do |faraday|
  faraday.ssl[:ca_file] = '/path/to/custom-ca-bundle.pem'
end

twilio.http_client.adapter = :happy_eyeballs

For Account SID plus Auth Token authentication, use:

twilio = Twilio::REST::Client.new(, auth_token)
twilio.http_client.adapter = :happy_eyeballs

IPv4-Only Preference

client = Faraday.new('https://example.com') do |faraday|
  faraday.adapter :happy_eyeballs, {
    prefer_ipv6: false,              # Prefer IPv4
    connection_attempt_delay: 0.1,   # Faster attempts for IPv4 preference
    first_address_family_count: 2    # Try more IPv4 addresses first
  }
end

Configuration Options

Option Type Default Description
connection_attempt_delay Float 0.25 Delay in seconds between connection attempts
resolution_delay Float 0.05 Delay in seconds between IPv6 and IPv4 DNS resolution
first_address_family_count Integer 1 Number of preferred address family addresses to try first
connection_timeout Float 10 Total timeout in seconds for all connection attempts
cache_ttl Integer 300 Time-to-live in seconds for cached connection preferences
prefer_ipv6 Boolean true Whether to prefer IPv6 addresses (RFC 6555 recommendation)
enable_cache Boolean true Whether to cache successful connection results
ca_file String detected CA bundle path used for HTTPS certificate verification

How It Works

  1. DNS Resolution: Simultaneously resolves both IPv4 (A) and IPv6 (AAAA) records with a small delay favoring the preferred protocol
  2. Address Sorting: Sorts addresses according to preference (IPv6 first by default, as per RFC)
  3. Connection Racing: Attempts connections with staggered timing:
    • Starts with the first address of the preferred family
    • After a delay, starts attempting alternate addresses
    • Continues until a connection succeeds or all attempts fail
  4. HTTP Request: Uses the successful connection to make the actual HTTP request
  5. Caching: Caches successful connection information to speed up future requests

For HTTPS, the adapter preserves the original hostname for SNI and certificate hostname verification while dialing the address selected by the connection race. Certificate verification remains enabled.

Error Handling

The adapter raises standard Faraday exceptions:

  • Faraday::ConnectionFailed: When all connection attempts fail
  • Faraday::TimeoutError: When the overall connection timeout is exceeded
begin
  response = client.get('/')
rescue Faraday::ConnectionFailed => e
  puts "All connections failed: #{e.message}"
rescue Faraday::TimeoutError => e
  puts "Connection timed out: #{e.message}"
end

Performance Considerations

  • First Request: May be slightly slower due to DNS resolution and connection racing
  • Subsequent Requests: Faster due to connection caching
  • Memory Usage: Minimal - only caches successful connection metadata
  • Thread Safety: Safe for concurrent use across multiple threads

Compatibility

  • Ruby: >= 2.7.0
  • Faraday: >= 1.0
  • IPv6 Support: Not required. The adapter resolves and races whichever address families the host and DNS actually provide - it works correctly on IPv4-only hosts (the common case), IPv6-only hosts, and dual-stack hosts alike.

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Testing

bundle exec rspec

Debug

export DEBUG_HAPPYEYES='true' # puts progress

License

The gem is available as open source under the terms of the MIT License.

References