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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions