Project

General

Profile

Actions

Feature #21642

open

Introduce `IO::ConnectionResetError` and `IO::BrokenPipeError` as standardized IO-level exceptions.

Feature #21642: Introduce `IO::ConnectionResetError` and `IO::BrokenPipeError` as standardized IO-level exceptions.

Added by ioquatix (Samuel Williams) about 24 hours ago. Updated about 18 hours ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:<unknown>]

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 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:

    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 or Errno::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) about 24 hours ago Actions #1

  • Description updated (diff)

Updated by ioquatix (Samuel Williams) about 24 hours ago Actions #2

  • 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) about 22 hours ago Actions #3

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) about 19 hours ago · Edited Actions #4

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 18 hours ago Actions #5

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

Actions

Also available in: PDF Atom