A Faraday adapter that implements the Happy Eyeballs algorithm (RFC 6555) for intelligent IPv4/IPv6 dual-stack connections.
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.
- 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
Add this line to your application's Gemfile:
gem 'faraday-happy-eyeballs'And then execute:
$ bundle installOr install it yourself as:
$ gem install faraday-happy-eyeballsrequire 'faraday'
require 'faraday/happy_eyeballs'
client = Faraday.new('https://example.com') do |faraday|
faraday.adapter :happy_eyeballs
end
response = client.get('/')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
endclient = Faraday.new('https://example.com') do |faraday|
faraday.use :retry, max: 3
faraday.use :gzip
faraday.adapter :happy_eyeballs, prefer_ipv6: false
endThe 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, account_sid)
twilio.http_client.configure_connection do |faraday|
faraday.ssl[:ca_file] = '/path/to/custom-ca-bundle.pem'
end
twilio.http_client.adapter = :happy_eyeballsFor Account SID plus Auth Token authentication, use:
twilio = Twilio::REST::Client.new(account_sid, auth_token)
twilio.http_client.adapter = :happy_eyeballsclient = 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| 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 |
- DNS Resolution: Simultaneously resolves both IPv4 (A) and IPv6 (AAAA) records with a small delay favoring the preferred protocol
- Address Sorting: Sorts addresses according to preference (IPv6 first by default, as per RFC)
- 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
- HTTP Request: Uses the successful connection to make the actual HTTP request
- 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.
The adapter raises standard Faraday exceptions:
Faraday::ConnectionFailed: When all connection attempts failFaraday::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- 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
- 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.
- Fork the repository
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
bundle exec rspecexport DEBUG_HAPPYEYES='true' # puts progress
The gem is available as open source under the terms of the MIT License.