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, 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_eyeballs
For Account SID plus Auth Token authentication, use:
twilio = Twilio::REST::Client.new(account_sid, 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
- 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.
Error Handling
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.}"
rescue Faraday::TimeoutError => e
puts "Connection timed out: #{e.}"
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
- 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
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.