Problem
Client.cutoff currently represents both receive EOF and connection-wide terminality. When the peer half-closes its write direction, Client.receive() correctly observes EOF and sets cutoff:
if data:
...
else:
self.cutoff = True
However, Client.serviceSends() also uses cutoff to disable output:
while self.txbs and self.connected and not self.cutoff:
count = self.send(self.txbs)
del self.txbs[:count]
TCP EOF is directional. A peer calling shutdown(SHUT_WR) means no more bytes can arrive from that peer; it does not prevent the Client from sending a response through the opposite direction.
The current behavior therefore strands output queued after receive EOF in Client.txbs.
Reproduction
- Connect a
Client to one side of a socket pair.
- Have the peer call
shutdown(socket.SHUT_WR).
- Service Client receives and observe EOF.
- Queue a response after EOF.
- Service Client sends.
- Observe that the response remains in
txbs and never reaches the peer.
This test fails on current main at assert not client.txbs:
import socket
from hio.core import tcp
def test_client_sends_after_real_peer_half_close():
cs, peer = socket.socketpair()
peer.settimeout(1.0)
client = tcp.Client(ha=("127.0.0.1", 6101))
client.cs = cs
client.accepted = True
try:
peer.shutdown(socket.SHUT_WR)
client.serviceReceives()
assert client.cutoff is True
response = b"response after peer EOF"
client.tx(response)
client.serviceSends()
assert not client.txbs
assert peer.recv(len(response)) == response
finally:
client.close()
peer.close()
Expected behavior
Receiving clean peer EOF should close only the Client receive direction. The Client should remain able to accept and transmit output until its send direction is independently closed or fails.
Proposed solution
Track receive and transmit terminal state independently. One implementation uses cutoff for receive closure and adds a separate transmit-closure state:
self.cutoff = False
self.txCutoff = False
self.error = None
Receive EOF changes only the receive state:
if not data:
self.cutoff = True
Send service is governed by the transmit state:
while self.txbs and self.connected and not self.txCutoff:
count = self.send(self.txbs)
del self.txbs[:count]
break
A terminal send failure closes only the transmit direction where appropriate and preserves the failure and unsent bytes:
except BrokenPipeError as ex:
self.txCutoff = True
self.error = ex
count = 0
The exact state representation need not be named txCutoff, but it must preserve the directional invariant: receive EOF alone must not disable sending.
Acceptance criteria
- Peer
SHUT_WR marks the Client receive direction closed without marking transmit closed.
- Output queued before or after receive EOF can still drain.
- The peer receives that output over a real socket.
- Terminal send failure preserves unsent bytes and is represented independently from receive EOF.
- Allocating a new connection resets both directional terminal states.
Problem
Client.cutoffcurrently represents both receive EOF and connection-wide terminality. When the peer half-closes its write direction,Client.receive()correctly observes EOF and setscutoff:However,
Client.serviceSends()also usescutoffto disable output:TCP EOF is directional. A peer calling
shutdown(SHUT_WR)means no more bytes can arrive from that peer; it does not prevent theClientfrom sending a response through the opposite direction.The current behavior therefore strands output queued after receive EOF in
Client.txbs.Reproduction
Clientto one side of a socket pair.shutdown(socket.SHUT_WR).txbsand never reaches the peer.This test fails on current
mainatassert not client.txbs:Expected behavior
Receiving clean peer EOF should close only the Client receive direction. The Client should remain able to accept and transmit output until its send direction is independently closed or fails.
Proposed solution
Track receive and transmit terminal state independently. One implementation uses
cutofffor receive closure and adds a separate transmit-closure state:Receive EOF changes only the receive state:
Send service is governed by the transmit state:
A terminal send failure closes only the transmit direction where appropriate and preserves the failure and unsent bytes:
The exact state representation need not be named
txCutoff, but it must preserve the directional invariant: receive EOF alone must not disable sending.Acceptance criteria
SHUT_WRmarks the Client receive direction closed without marking transmit closed.