Skip to content

Don't report queued datagrams to the protocol after UDPTransport.abort() - #772

Open
graingert wants to merge 6 commits into
MagicStack:masterfrom
graingert:fix-udp-abort-queued-datagram
Open

graingert wants to merge 6 commits into
MagicStack:masterfrom
graingert:fix-udp-abort-queued-datagram

Conversation

@graingert

@graingert graingert commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #771.

Calling abort() on a UDPTransport with a datagram queued (the OS refused it with EAGAIN) reported two errors to the loop's exception handler. Vanilla asyncio silently discards the datagram.

When the handle closes, libuv cancels queued sends with UV_ECANCELED. The send callback turned that into a CancelledError "Fatal write error", then called _maybe_resume_protocol() after the protocol had already been cleared. That raised the resume_writing() AttributeError.

A second difference turned up while fixing this. A queued datagram can still be sent between abort() and the scheduled connection_lost(). uvloop then called resume_writing() on the aborted protocol, which asyncio never does, because its _force_close clears the buffer.

Changes:

  • __uv_udp_on_send drops sends that were cancelled because the handle is closed.
  • UDPTransport._on_sent reports nothing to the protocol once _conn_lost is set, i.e. after abort() or a fatal error. A graceful close() still flushes the queue and resumes the protocol, as asyncio does.
  • UDPTransport._close stops receiving, as UVStream._close does. The transport holds a reference to itself while receiving, so a UDP transport still open at loop.close() was never freed. The new loop-close test caught this through the debug build's handle-count check.

Tests run on both uvloop and asyncio. They use a UNIX datagram socket, because UDP over loopback never refuses a datagram. The asyncio variants are skipped on macOS: there the socket refuses datagrams with ENOBUFS, which libuv treats like EAGAIN and queues, but asyncio reports to error_received() and drops.

Tests:

  • test_abort_with_queued_datagram: the case from the issue.

  • test_abort_with_queued_datagram_then_writable: the peer makes room before the loop runs again, and there is no resume_writing() after abort().

  • test_close_with_queued_datagram_then_writable: close() behaviour is unchanged.

  • test_loop_close_with_queued_datagram (uvloop only): closing the loop with the transport still open cancels the queued send. On master this reports a fatal CancelledError and calls resume_writing(). The test covers the UV_ECANCELED check, which the abort tests don't reach because _conn_lost is already set.

All four tests fail on uvloop without this change.

🤖 Generated with Claude Code

graingert and others added 6 commits October 5, 2026 10:54
Aborting a UDPTransport while a datagram was queued reported a fatal
CancelledError and a resume_writing() AttributeError to the loop's
exception handler, because libuv cancels queued sends when the handle
closes and the send callback treated that as an error after the
protocol had been detached.

Also, a queued datagram that was sent between abort() and the
transport closing still resumed the protocol. Like asyncio, stop
reporting send results to the protocol once the connection is lost.

Fixes MagicStack#771

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UV_ECANCELED check in the send callback was only exercised by
closing the loop with a UDP transport still open, as abort() is now
handled by the _conn_lost check in _on_sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UDPTransport keeps a reference to itself while receiving, which was
only dropped by _stop_reading(). loop.close() closes leftover handles
with _close(), so a UDP transport still open at that point was never
freed. Stop reading in _close(), as UVStream does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests sent datagrams until the transport queued one, which never
happened on macOS CI and spun until the job was killed. Use a small
send buffer, as anyio's UNIX datagram tests do, and give up after a
bounded number of sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS refuses UNIX datagrams with ENOBUFS rather than EAGAIN. libuv
treats ENOBUFS like EAGAIN and queues the datagram, but asyncio reports
it to error_received() and drops it, so the send queue never fills.
The small send buffer did not change that, so drop it again.

Co-Authored-By: Claude Opus 5.5 <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

Development

Successfully merging this pull request may close these issues.

UDPTransport.abort() with a queued datagram reports a fatal write error and a resume_writing() AttributeError

1 participant