-
Notifications
You must be signed in to change notification settings - Fork 254
Description
Summary
On Ruby 4.0.0, Net::LDAP#bind may raise IO::TimeoutError directly when connect_timeout is used,
instead of being wrapped as Net::LDAP::Error.
With the same net-ldap version (0.20.0), Ruby 3.4.7 wraps the timeout as Net::LDAP::Error,
so this appears to be a behavioral regression caused by changes in Ruby 4.0.
This breaks existing code that rescues Net::LDAP::Error for connection failures.
Steps to reproduce
$ ruby -v
# ruby 3.4.7
# ruby 4.0.0
$ gem install net-ldap -v 0.20.0# reproduce.rb
require "net/ldap"
ldap = Net::LDAP.new(
host: "example.com",
port: 389,
connect_timeout: 1,
auth: {
method: :simple,
username: "cn=dummy,dc=example,dc=com",
password: "dummy",
},
)
ldap.bind$ ruby reproduce.rbExpected behavior (Ruby 3.4.7)
With net-ldap 0.20.0 on Ruby 3.4.7, connection timeouts are wrapped by net-ldap and raised as
Net::LDAP::Error from Net::LDAP::Connection#open_connection.
Example stack trace:
/path/to/gems/net-ldap-0.20.0/lib/net/ldap/connection.rb:72:in `Net::LDAP::Connection#open_connection':
Connection timed out - user specified timeout (Net::LDAP::Error)
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap/connection.rb:736:in `Net::LDAP::Connection#socket'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap.rb:1349:in `Net::LDAP#new_connection'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap.rb:869:in `block in Net::LDAP#bind'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap/instrumentation.rb:19:in `Net::LDAP::Instrumentation#instrument'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap.rb:863:in `Net::LDAP#bind'
from reproduce.rb:14:in `<main>'
This behavior is considered the expected behavior for compatibility, as existing applications
commonly rescue Net::LDAP::Error.
Actual behavior (Ruby 4.0.0)
On Ruby 4.0.0, IO::TimeoutError is raised directly from Ruby’s socket implementation and is not
wrapped by net-ldap:
/path/to/ruby/4.0.0/lib/ruby/4.0.0/socket.rb:923:in `block in Socket.tcp_with_fast_fallback':
user specified timeout for example.com:389 (IO::TimeoutError)
from /path/to/ruby/4.0.0/lib/ruby/4.0.0/socket.rb:731:in `Kernel#loop'
from /path/to/ruby/4.0.0/lib/ruby/4.0.0/socket.rb:731:in `Socket.tcp_with_fast_fallback'
from /path/to/ruby/4.0.0/lib/ruby/4.0.0/socket.rb:669:in `Socket.tcp'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap/connection.rb:747:in `Net::LDAP::Connection::DefaultSocket.new'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap/connection.rb:53:in `block in Net::LDAP::Connection#open_connection'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap/connection.rb:51:in `Array#each'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap/connection.rb:51:in `Net::LDAP::Connection#open_connection'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap/connection.rb:736:in `Net::LDAP::Connection#socket'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap.rb:1349:in `Net::LDAP#new_connection'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap.rb:869:in `block in Net::LDAP#bind'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap/instrumentation.rb:19:in `Net::LDAP::Instrumentation#instrument'
from /path/to/gems/net-ldap-0.20.0/lib/net/ldap.rb:863:in `Net::LDAP#bind'
from reproduce.rb:14:in `<main>'
Why this happens (analysis)
Net::LDAP::Connection#open_connection rescues the following exceptions:
Net::LDAP::ErrorSocketErrorSystemCallErrorOpenSSL::SSL::SSLError
On Ruby 4.0.0, Socket.tcp(..., connect_timeout: ...) may raise IO::TimeoutError
(from Socket.tcp_with_fast_fallback), which is not a subclass of SystemCallError.
As a result, the exception bypasses the rescue clause and bubbles up to the caller.
This behavior change appears to be triggered by a change in Ruby 4.0's socket timeout handling.
In particular, Ruby PR #15582 modified how user-specified timeouts are handled in Socket.tcp,
causing IO::TimeoutError to be raised for connection timeouts.
Reference:
ruby/ruby#15582
Proposed fix
Include IO::TimeoutError in the rescue list of Net::LDAP::Connection#open_connection:
rescue Net::LDAP::Error,
SocketError,
SystemCallError,
OpenSSL::SSL::SSLError,
+ IO::TimeoutError => eSystem configuration
- Ruby: 3.4.7 (expected), 4.0.0 (actual)
- net-ldap: 0.20.0