[ruby-core:123496] [Ruby Feature#21642] Introduce `IO::ConnectionReset` and `IO::BrokenPipe` as standardized IO-level exceptions.
Issue #21642 has been reported by ioquatix (Samuel Williams). ---------------------------------------- Feature #21642: Introduce `IO::ConnectionReset` and `IO::BrokenPipe` as standardized IO-level exceptions. https://bugs.ruby-lang.org/issues/21642 * Author: ioquatix (Samuel Williams) * Status: Open ---------------------------------------- Currently, different IO implementations in Ruby raise inconsistent exception types when a connection is reset or broken. For example: ```ruby # Plain TCP socket: socket.read_nonblock(1024) # => Errno::ECONNRESET # SSL socket: ssl_socket.read_nonblock(1024) # => OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading ``` Both represent a *connection reset by peer*, but the errors differ significantly in type and message. This inconsistency makes it difficult to handle connection-level errors generically across IO types. Similarly, `EPIPE` is used in some contexts to signal a *broken connection*, but again, the representation and message differ between IO classes. ### Proposal Introduce explicit subclasses of the corresponding system errors as part of Ruby’s standard IO interface: ```ruby class IO class ConnectionReset < Errno::ECONNRESET; end class BrokenPipe < Errno::EPIPE; end end ``` Then, standardize the Ruby I/O ecosystem (including OpenSSL) to raise these subclasses instead of raw system errors or library-specific error wrappers. This would establish a consistent, well-defined public interface for handling connection-level failures. ### Motivation * **Consistency:** Users can handle `IO::ConnectionReset` across `IO`, `TCPSocket`, `OpenSSL::SSL::SSLSocket`, and other IO-like objects. * **Clarity:** The name clearly expresses a high-level semantic (“connection reset”) rather than a low-level system error. * **Extensibility:** Other Ruby IO implementations (custom sockets, pipes, etc.) can follow the same convention. * **Backwards Compatibility:** Because `IO::ConnectionReset < Errno::ECONNRESET`, existing rescue clauses continue to work: ```ruby rescue Errno::ECONNRESET # still catches it end ``` ### Examples ```ruby begin io.read_nonblock(1024) rescue IO::ConnectionReset puts "Connection was reset by peer." rescue IO::BrokenPipe puts "Connection was broken (EPIPE)." end ``` ### Impact on existing code * Minimal to none. * Existing code that rescues `Errno::ECONNRESET` or `Errno::EPIPE` will continue to function. * Future code gains a more semantic and portable way to handle these common failure modes. -- https://bugs.ruby-lang.org/
Issue #21642 has been updated by mame (Yusuke Endoh). I sympathize with this proposal. Having to rescue a wide variety of exceptions is a common pain point, especially when writing applications like chatbots. I believe the best approach is to classify exceptions based on the required user action, rather than by their underlying cause. The existing `IO::WaitReadable` and `IO::WaitWritable` modules are excellent precedents for this principle. They unify various underlying exceptions (e.g., `Errno::EAGAIN`, `Errno::EWOULDBLOCK`, `OpenSSL::SSLErrorWaitReadable`) under a single concept. In all these cases, the user's action is the same: wait for the IO object to become ready. This is a very rational approach. Applying this same logic to `ConnectionResetError` and `BrokenPipeError`, the user's response is almost always to treat the connection as unrecoverable and call `IO#close`. To parallel the naming convention of `IO::WaitReadable` (which is based on the action "wait"), a name like `IO::Close` or `IO::CloseUnrecoverable` seems like a consistent and descriptive choice, as it directly reflects the required action "close". While the exact name is certainly open for discussion, it follows this established pattern. Furthermore, I believe using this should be an includable module rather than part of a class hierarchy, mirroring the design of `IO::WaitReadable`. Changing existing code that raises `OpenSSL::SSL::SSLError` to raise a different exception class would be a major backward incompatibility. A much safer approach would be to have `OpenSSL::SSL::SSLError` simple `include` the `IO::CloseUnrecoverable` module. ---------------------------------------- Feature #21642: Introduce `IO::ConnectionResetError` and `IO::BrokenPipeError` as standardized IO-level exceptions. https://bugs.ruby-lang.org/issues/21642#change-114870 * Author: ioquatix (Samuel Williams) * Status: Open ---------------------------------------- Currently, different IO implementations in Ruby raise inconsistent exception types when a connection is reset or broken. For example: ```ruby # Plain TCP socket: socket.read_nonblock(1024) # => Errno::ECONNRESET # SSL socket: ssl_socket.read_nonblock(1024) # => OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading ``` Both represent a *connection reset by peer*, but the errors differ significantly in type and message. This inconsistency makes it difficult to handle connection-level errors generically across IO types. Similarly, `EPIPE` is used in some contexts to signal a *broken connection*, but again, the representation and message differ between IO classes. ### Proposal Introduce explicit subclasses of the corresponding system errors as part of Ruby’s standard IO interface: ```ruby class IO class ConnectionResetError < Errno::ECONNRESET; end class BrokenPipeError < Errno::EPIPE; end end ``` Then, standardize the Ruby I/O ecosystem (including OpenSSL) to raise these subclasses instead of raw system errors or library-specific error wrappers. This would establish a consistent, well-defined public interface for handling connection-level failures. ### Motivation * **Consistency:** Users can handle `IO::ConnectionResetError` across `IO`, `TCPSocket`, `OpenSSL::SSL::SSLSocket`, and other IO-like objects. * **Clarity:** The name clearly expresses a high-level semantic (“connection reset”) rather than a low-level system error. * **Extensibility:** Other Ruby IO implementations (custom sockets, pipes, etc.) can follow the same convention. * **Backwards Compatibility:** Because `IO::ConnectionResetError < Errno::ECONNRESET`, existing rescue clauses continue to work: ```ruby rescue Errno::ECONNRESET # still catches it end ``` ### Examples ```ruby begin io.read_nonblock(1024) rescue IO::ConnectionResetError puts "Connection was reset by peer." end ``` ### Impact on existing code * Minimal to none. * Existing code that rescues `Errno::ECONNRESET` or `Errno::EPIPE` will continue to function. * Future code gains a more semantic and portable way to handle these common failure modes. -- https://bugs.ruby-lang.org/
Issue #21642 has been updated by ioquatix (Samuel Williams). Thanks for your feedback. I understand your point and I think it makes sense. "Connection Reset" and "Broken Pipe" have well defined meanings. On the face of it, I don't know what "CloseUnrecoverable" means. Is there a concept like this in other languages? - "Connection Reset" occurs during read, and indicates that the remote end has dropped the connection, and it's likely that we are missing data (as opposed to reaching end of stream). - "Broken Pipe" occurs during write, and indicates the remote end is no longer accepting more data. Broken pipe doesn't mean you can't read more data. So there are subtle differences. Modules like `IO::BrokenPipe` and `IO::ConnectionReset` might be suitable, but maybe they are too specific? A lot of UNIXisms are overly specific (like `UNIXSocket` on Windows doesn't really make sense). If you felt like having a concept for "The connection has failed in an unrecoverable way, perhaps `module IO::StreamFailed`. Regarding OpenSSL, there are many places in OpenSSL that raise `SSLError`, so I think what you are suggesting is this: ```ruby class SSLReadError < SSLError include IO::ConnectionReset end ``` Is it sufficiently compatible? My main issue is not that we need to introduce shared concepts, it's that OpenSSL does not implement a compatible interface to `IO#read` and `IO#write`. But I think expecting OpenSSL to raise `Errno::EPIPE` and `Errno::ECONNRESET` is also unrealistic/incorrect. ---------------------------------------- Feature #21642: Introduce `IO::ConnectionResetError` and `IO::BrokenPipeError` as standardized IO-level exceptions. https://bugs.ruby-lang.org/issues/21642#change-114871 * Author: ioquatix (Samuel Williams) * Status: Open ---------------------------------------- Currently, different IO implementations in Ruby raise inconsistent exception types when a connection is reset or broken. For example: ```ruby # Plain TCP socket: socket.read_nonblock(1024) # => Errno::ECONNRESET # SSL socket: ssl_socket.read_nonblock(1024) # => OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading ``` Both represent a *connection reset by peer*, but the errors differ significantly in type and message. This inconsistency makes it difficult to handle connection-level errors generically across IO types. Similarly, `EPIPE` is used in some contexts to signal a *broken connection*, but again, the representation and message differ between IO classes. ### Proposal Introduce explicit subclasses of the corresponding system errors as part of Ruby’s standard IO interface: ```ruby class IO class ConnectionResetError < Errno::ECONNRESET; end class BrokenPipeError < Errno::EPIPE; end end ``` Then, standardize the Ruby I/O ecosystem (including OpenSSL) to raise these subclasses instead of raw system errors or library-specific error wrappers. This would establish a consistent, well-defined public interface for handling connection-level failures. ### Motivation * **Consistency:** Users can handle `IO::ConnectionResetError` across `IO`, `TCPSocket`, `OpenSSL::SSL::SSLSocket`, and other IO-like objects. * **Clarity:** The name clearly expresses a high-level semantic (“connection reset”) rather than a low-level system error. * **Extensibility:** Other Ruby IO implementations (custom sockets, pipes, etc.) can follow the same convention. * **Backwards Compatibility:** Because `IO::ConnectionResetError < Errno::ECONNRESET`, existing rescue clauses continue to work: ```ruby rescue Errno::ECONNRESET # still catches it end ``` ### Examples ```ruby begin io.read_nonblock(1024) rescue IO::ConnectionResetError puts "Connection was reset by peer." end ``` ### Impact on existing code * Minimal to none. * Existing code that rescues `Errno::ECONNRESET` or `Errno::EPIPE` will continue to function. * Future code gains a more semantic and portable way to handle these common failure modes. -- https://bugs.ruby-lang.org/
Issue #21642 has been updated by mame (Yusuke Endoh). ioquatix (Samuel Williams) wrote in #note-4:
- "Connection Reset" occurs during read, and indicates that the remote end has dropped the connection, and it's likely that we are missing data (as opposed to reaching end of stream).
I see, so it's still possible to "write" as well as "close".
So there are subtle differences. Modules like `IO::BrokenPipe` and `IO::ConnectionReset` might be suitable, but maybe they are too specific?
Both `BrokenPipe` and `ConnectionReset` only convey the nuance of "no further I/O possible" to me, so I couldn't understand the difference. While not user-action-based names, something like `IO::ReadError` and `IO::WriteError` would be clearer to me, though I'm unsure if they're appropriate. As I recall, @akr proposed and introduced `IO::WaitReadable`. I would like to hear his opinion.
Regarding OpenSSL, there are many places in OpenSSL that raise `SSLError`, so I think what you are suggesting is this:
```ruby class SSLReadError < SSLError include IO::ConnectionReset end ```
Is it sufficiently compatible?
Looks good to me. ---------------------------------------- Feature #21642: Introduce `IO::ConnectionResetError` and `IO::BrokenPipeError` as standardized IO-level exceptions. https://bugs.ruby-lang.org/issues/21642#change-114873 * Author: ioquatix (Samuel Williams) * Status: Open ---------------------------------------- Currently, different IO implementations in Ruby raise inconsistent exception types when a connection is reset or broken. For example: ```ruby # Plain TCP socket: socket.read_nonblock(1024) # => Errno::ECONNRESET # SSL socket: ssl_socket.read_nonblock(1024) # => OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading ``` Both represent a *connection reset by peer*, but the errors differ significantly in type and message. This inconsistency makes it difficult to handle connection-level errors generically across IO types. Similarly, `EPIPE` is used in some contexts to signal a *broken connection*, but again, the representation and message differ between IO classes. ### Proposal Introduce explicit subclasses of the corresponding system errors as part of Ruby’s standard IO interface: ```ruby class IO class ConnectionResetError < Errno::ECONNRESET; end class BrokenPipeError < Errno::EPIPE; end end ``` Then, standardize the Ruby I/O ecosystem (including OpenSSL) to raise these subclasses instead of raw system errors or library-specific error wrappers. This would establish a consistent, well-defined public interface for handling connection-level failures. ### Motivation * **Consistency:** Users can handle `IO::ConnectionResetError` across `IO`, `TCPSocket`, `OpenSSL::SSL::SSLSocket`, and other IO-like objects. * **Clarity:** The name clearly expresses a high-level semantic (“connection reset”) rather than a low-level system error. * **Extensibility:** Other Ruby IO implementations (custom sockets, pipes, etc.) can follow the same convention. * **Backwards Compatibility:** Because `IO::ConnectionResetError < Errno::ECONNRESET`, existing rescue clauses continue to work: ```ruby rescue Errno::ECONNRESET # still catches it end ``` ### Examples ```ruby begin io.read_nonblock(1024) rescue IO::ConnectionResetError puts "Connection was reset by peer." end ``` ### Impact on existing code * Minimal to none. * Existing code that rescues `Errno::ECONNRESET` or `Errno::EPIPE` will continue to function. * Future code gains a more semantic and portable way to handle these common failure modes. -- https://bugs.ruby-lang.org/
Issue #21642 has been updated by mame (Yusuke Endoh). I talked with @akr-san. He said it should be first confirmed whether you have any real use cases that actually want to handle situations like "can't read but can write" (or vice versa). Pipes are typically unidirectional, so when a write operation throws EPIPE, it means the pipe is write-only, so read operations does not work regardless of EPIPE. TCP sockets have the concept of half-close, but this is difficult to use correctly. If the only action a user can take is "close" in most cases, it would be good to introduce only one module and consider the good name, @akr said. ---------------------------------------- Feature #21642: Introduce `IO::ConnectionResetError` and `IO::BrokenPipeError` as standardized IO-level exceptions. https://bugs.ruby-lang.org/issues/21642#change-114880 * Author: ioquatix (Samuel Williams) * Status: Open ---------------------------------------- Currently, different IO implementations in Ruby raise inconsistent exception types when a connection is reset or broken. For example: ```ruby # Plain TCP socket: socket.read_nonblock(1024) # => Errno::ECONNRESET # SSL socket: ssl_socket.read_nonblock(1024) # => OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading ``` Both represent a *connection reset by peer*, but the errors differ significantly in type and message. This inconsistency makes it difficult to handle connection-level errors generically across IO types. Similarly, `EPIPE` is used in some contexts to signal a *broken connection*, but again, the representation and message differ between IO classes. ### Proposal Introduce explicit subclasses of the corresponding system errors as part of Ruby’s standard IO interface: ```ruby class IO class ConnectionResetError < Errno::ECONNRESET; end class BrokenPipeError < Errno::EPIPE; end end ``` Then, standardize the Ruby I/O ecosystem (including OpenSSL) to raise these subclasses instead of raw system errors or library-specific error wrappers. This would establish a consistent, well-defined public interface for handling connection-level failures. ### Motivation * **Consistency:** Users can handle `IO::ConnectionResetError` across `IO`, `TCPSocket`, `OpenSSL::SSL::SSLSocket`, and other IO-like objects. * **Clarity:** The name clearly expresses a high-level semantic (“connection reset”) rather than a low-level system error. * **Extensibility:** Other Ruby IO implementations (custom sockets, pipes, etc.) can follow the same convention. * **Backwards Compatibility:** Because `IO::ConnectionResetError < Errno::ECONNRESET`, existing rescue clauses continue to work: ```ruby rescue Errno::ECONNRESET # still catches it end ``` ### Examples ```ruby begin io.read_nonblock(1024) rescue IO::ConnectionResetError puts "Connection was reset by peer." end ``` ### Impact on existing code * Minimal to none. * Existing code that rescues `Errno::ECONNRESET` or `Errno::EPIPE` will continue to function. * Future code gains a more semantic and portable way to handle these common failure modes. -- https://bugs.ruby-lang.org/
participants (2)
-
ioquatix (Samuel Williams) -
mame (Yusuke Endoh)