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

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>

This branch has not been deployed

No deployments
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