Skip to content

Ruby 4.0: connect_timeout raises IO::TimeoutError instead of Net::LDAP::Error #442

@k-tsuchiya-jp

Description

@k-tsuchiya-jp

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.rb

Expected 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::Error
  • SocketError
  • SystemCallError
  • OpenSSL::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 => e

System configuration

  • Ruby: 3.4.7 (expected), 4.0.0 (actual)
  • net-ldap: 0.20.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions