Feature #21642
openIntroduce `IO::ConnectionResetError` and `IO::BrokenPipeError` as standardized IO-level exceptions.
Description
Currently, different IO implementations in Ruby raise inconsistent exception types when a connection is reset or broken.
For example:
# 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:
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
acrossIO
,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:rescue Errno::ECONNRESET # still catches it end
Examples¶
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
orErrno::EPIPE
will continue to function. - Future code gains a more semantic and portable way to handle these common failure modes.
Updated by ioquatix (Samuel Williams) 1 day ago
- Description updated (diff)
Updated by ioquatix (Samuel Williams) 1 day ago
- Subject changed from Introduce `IO::ConnectionReset` and `IO::BrokenPipe` as standardized IO-level exceptions. to Introduce `IO::ConnectionResetError` and `IO::BrokenPipeError` as standardized IO-level exceptions.
Updated by mame (Yusuke Endoh) 1 day ago
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.
Updated by ioquatix (Samuel Williams) 1 day ago
· Edited
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. The remote end might have invoked
close_write
rather than closing the entire connection (in fact this is fairly common).
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
(since these only apply to streams).
Regarding OpenSSL, there are many places in OpenSSL that raise SSLError
, so I think what you are suggesting is this:
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.
Updated by mame (Yusuke Endoh) about 24 hours ago
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
andIO::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 (Akira Tanaka) 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:class SSLReadError < SSLError include IO::ConnectionReset end
Is it sufficiently compatible?
Looks good to me.
Updated by mame (Yusuke Endoh) about 1 hour ago
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 (Akira Tanaka) said.