Problem
Established ClientTls and RemoterTls sessions classify send failures through a broad OSError branch that mixes integer error values with exception classes:
except OSError as ex:
if ex.args[0] in (
ssl.SSL_ERROR_WANT_READ,
ssl.SSL_ERROR_WANT_WRITE,
):
result = 0
elif ex.args[0] in (..., ssl.SSLEOFError):
self.cutoff = True
result = 0
This obscures whether an operation should be retried, only transmission has failed, or the complete TLS transport is unusable. It also leaves callers without the original failure, permits later writes to enter a dead queue, and can strand accepted plaintext without an observable cause.
This issue concerns established-session data-plane I/O. Authenticated peer closure and abrupt receive EOF are tracked by #173.
Required outcome model
| Send outcome |
Retry |
Receive |
Transmit |
Queued plaintext |
Cause |
SSLWantReadError / SSLWantWriteError |
Yes |
Open |
Open |
Preserve |
None |
EAGAIN / EWOULDBLOCK |
Yes |
Open |
Open |
Preserve |
None |
BrokenPipeError |
No |
Remains available |
Closed |
Preserve |
Retain |
SSLEOFError / SSLSyscallError |
No |
Closed |
Closed |
Preserve |
Retain and raise |
| Connection-wide socket failure |
No |
Closed |
Closed |
Preserve |
Retain and raise |
| Other terminal send failure |
No |
Available unless known otherwise |
Closed |
Preserve |
Retain and raise |
A TLS write may require the underlying socket to become readable, and a TLS read may require it to become writable. WANT_READ and WANT_WRITE are therefore recurrence signals, not terminal outcomes. The same plaintext operation must be retried later without deleting or duplicating application data.
A broken write is directional: it prevents further transmission but does not prove that final peer input is unavailable. In contrast, TLS protocol failure, connection reset, network reset, timeout, and equivalent connection-wide failures make both directions unusable.
Reproduction
- Create an established
ClientTls or RemoterTls with queued output.
- Make the wrapped socket raise
BrokenPipeError from send().
- Service sends.
- Observe that current
main raises without durable transmit state or a retained cause.
Repeat with typed WANT exceptions, EAGAIN, fatal SSL exceptions, and a representative connection-wide socket error. Each result should match the table above for both endpoint roles.
Proposed direction
Classify typed TLS exceptions before generic socket failures:
try:
count = self.cs.send(data)
except (ssl.SSLWantReadError, ssl.SSLWantWriteError):
count = 0
except BrokenPipeError as ex:
self.txCutoff = True
self.error = ex
count = 0
except (ssl.SSLEOFError, ssl.SSLSyscallError) as ex:
self.cutoff = self.txCutoff = True
self.error = ex
self.close()
raise
except OSError as ex:
if ex.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
count = 0
elif is_connection_wide(ex):
self.cutoff = self.txCutoff = True
self.error = ex
self.close()
raise
else:
self.txCutoff = True
self.error = ex
raise
Send service must delete only bytes accepted by the TLS socket. WANT and terminal failures therefore leave every unsent byte in txbs. Later tx() calls after transmit closure must follow #172's observable rejection contract.
State lifecycle
Terminal state and its retained cause belong to one socket generation. Opening a newly allocated connection must clear them; retrying the same failed socket must not. The endpoint must also preserve the first terminal cause rather than replacing it with a later late-enqueue error, because that first exception explains why transmission became impossible.
Related work
Acceptance criteria
- Both TLS endpoint roles use typed exception handling for send and receive WANT outcomes.
- WANT and blocked socket operations remain retryable without state or queue mutation.
- Broken pipe closes only transmit and preserves receive service.
- Fatal TLS and connection-wide failures close both directions and force-close.
- Every terminal outcome retains its original cause and every unsent byte.
- Later output is rejected after transmit closure.
- Deterministic coverage exercises the complete outcome table for both endpoint roles.
Problem
Established
ClientTlsandRemoterTlssessions classify send failures through a broadOSErrorbranch that mixes integer error values with exception classes:This obscures whether an operation should be retried, only transmission has failed, or the complete TLS transport is unusable. It also leaves callers without the original failure, permits later writes to enter a dead queue, and can strand accepted plaintext without an observable cause.
This issue concerns established-session data-plane I/O. Authenticated peer closure and abrupt receive EOF are tracked by #173.
Required outcome model
SSLWantReadError/SSLWantWriteErrorEAGAIN/EWOULDBLOCKBrokenPipeErrorSSLEOFError/SSLSyscallErrorA TLS write may require the underlying socket to become readable, and a TLS read may require it to become writable. WANT_READ and WANT_WRITE are therefore recurrence signals, not terminal outcomes. The same plaintext operation must be retried later without deleting or duplicating application data.
A broken write is directional: it prevents further transmission but does not prove that final peer input is unavailable. In contrast, TLS protocol failure, connection reset, network reset, timeout, and equivalent connection-wide failures make both directions unusable.
Reproduction
ClientTlsorRemoterTlswith queued output.BrokenPipeErrorfromsend().mainraises without durable transmit state or a retained cause.Repeat with typed WANT exceptions,
EAGAIN, fatal SSL exceptions, and a representative connection-wide socket error. Each result should match the table above for both endpoint roles.Proposed direction
Classify typed TLS exceptions before generic socket failures:
Send service must delete only bytes accepted by the TLS socket. WANT and terminal failures therefore leave every unsent byte in
txbs. Latertx()calls after transmit closure must follow #172's observable rejection contract.State lifecycle
Terminal state and its retained cause belong to one socket generation. Opening a newly allocated connection must clear them; retrying the same failed socket must not. The endpoint must also preserve the first terminal cause rather than replacing it with a later late-enqueue error, because that first exception explains why transmission became impossible.
Related work
Acceptance criteria