Skip to content

Handle plain OSError when waiting for the writer to close#362

Open
joanfabregat wants to merge 1 commit into
pgjones:mainfrom
joanfabregat:fix-oserror-on-close
Open

Handle plain OSError when waiting for the writer to close#362
joanfabregat wants to merge 1 commit into
pgjones:mainfrom
joanfabregat:fix-oserror-on-close

Conversation

@joanfabregat

Copy link
Copy Markdown

Fixes #361.

When a peer becomes unreachable mid-connection (e.g. EHOSTUNREACH — observed on Kubernetes when a client pod with open keep-alive connections is force-deleted), await self.writer.wait_closed() in TCPServer._close() raises a plain OSError, which the except tuple missed — CPython only maps ECONNRESET/EPIPE/ESHUTDOWN/ECONNABORTED/ECONNREFUSED to ConnectionError subclasses. The exception then either escapes the client_connected_cb task via run()'s finally: block ("Unhandled exception in client_connected_cb"), or propagates into the ASGI application's send path via protocol_send(Closed) — full tracebacks for both manifestations in #361.

Change

Catch OSError in _close(): since BrokenPipeError, ConnectionAbortedError and ConnectionResetError are all OSError subclasses, the tuple collapses to (OSError, RuntimeError, asyncio.CancelledError) — consistent with the except OSError: pass that run() already applies to the connection body. This is the same pattern as #134 (ConnectionAbortedError) and #172 (CancelledError).

A test is included: a MemoryWriter whose wait_closed() raises OSError(EHOSTUNREACH) — it fails on current main with the exact production traceback and passes with this change.

Verification

  • New test fails on unpatched main (raises OSError out of run()), passes with the fix
  • Full test suite: 197 passed (Python 3.12)
  • black --check, isort --check, flake8 clean

Notes

  • Complements Handle TimeoutError when waiting for the writer to close #342, which handles TimeoutError plus a transport.abort() at the same call site (since Python 3.11 TimeoutError is itself an OSError subclass, so this change also stops that error from escaping — but without the abort Handle TimeoutError when waiting for the writer to close #342 adds). Whichever lands second needs a trivial rebase.
  • The RawData branch of protocol_send() (except (ConnectionError, RuntimeError) around drain()) likely has the same EHOSTUNREACH blind spot, but it changes write-failure handling semantics so it's deliberately left out of this PR.

A peer that becomes unreachable mid-connection (e.g. EHOSTUNREACH when
the client host vanishes) raises a plain OSError from wait_closed(),
which the except tuple in TCPServer._close() missed - only
ConnectionError subclasses were caught. The exception then escaped the
client_connected_cb task via run()'s finally block, or propagated into
the ASGI application's send path via protocol_send(Closed).

Catch OSError instead: BrokenPipeError, ConnectionAbortedError and
ConnectionResetError are all OSError subclasses, so the tuple collapses
to (OSError, RuntimeError, asyncio.CancelledError) - consistent with
run()'s existing `except OSError`.

Fixes pgjones#361

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant