From 415551dd6a9c52c285b875fdcca2f77f761316cb Mon Sep 17 00:00:00 2001
From: Guflly <145608489+Guflly@users.noreply.github.com>
Date: Mon, 27 Jul 2026 00:06:31 -0700
Subject: [PATCH 1/4] Fix UDP socket close hangs on Windows
---
docs/versionhistory.rst | 2 ++
src/anyio/_backends/_asyncio.py | 4 ++++
tests/test_sockets.py | 16 ++++++++++++++++
3 files changed, 22 insertions(+)
diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index b5b8d01b9..e0205b40c 100644
--- a/docs/versionhistory.rst
+++ b/docs/versionhistory.rst
@@ -17,6 +17,8 @@ This library adheres to `Semantic Versioning 2.0 `_.
module name. (The default name for a task spawned with ``TaskGroup.start_soon`` or
``TaskGroup.start`` typically includes the module name.)
(`#1234 `_; PR by @gschaffner)
+- Fixed UDP socket closing hanging on Windows if a datagram send was still in flight
+ (`#1237 `_; PR by @Guflly)
- Fixed free-threading compatibility issues arising from the fact that on Python 3.14
free-threading builds, newly created threads inherit the current context by default,
causing AnyIO to behave erroneously in relation to ``start_blocking_portal()`` and
diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py
index 4fc1f0c64..9b91f5472 100644
--- a/src/anyio/_backends/_asyncio.py
+++ b/src/anyio/_backends/_asyncio.py
@@ -1687,6 +1687,8 @@ async def aclose(self) -> None:
self._closed = True
if not self._transport.is_closing():
self._transport.close()
+ await sleep(0)
+ self._transport.abort()
await self._protocol.closed_event.wait()
@@ -1737,6 +1739,8 @@ async def aclose(self) -> None:
self._closed = True
if not self._transport.is_closing():
self._transport.close()
+ await sleep(0)
+ self._transport.abort()
await self._protocol.closed_event.wait()
diff --git a/tests/test_sockets.py b/tests/test_sockets.py
index b456b4079..40c5ede6c 100644
--- a/tests/test_sockets.py
+++ b/tests/test_sockets.py
@@ -1743,6 +1743,14 @@ async def test_aclose_waits_for_fd_release(
udp = await UDPSocket.from_socket(sock)
await udp.aclose()
+ @pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
+ @pytest.mark.parametrize("anyio_backend", asyncio_params)
+ async def test_aclose_during_send(self) -> None:
+ udp = await create_udp_socket(local_host="127.0.0.1")
+ await udp.sendto(b"x", "127.0.0.1", 9999)
+ with fail_after(1):
+ await udp.aclose()
+
async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None:
async with await create_udp_socket(
family=family, local_host="localhost"
@@ -1914,6 +1922,14 @@ async def test_aclose_waits_for_fd_release(
finally:
peer.close()
+ @pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
+ @pytest.mark.parametrize("anyio_backend", asyncio_params)
+ async def test_aclose_during_send(self) -> None:
+ udp = await create_connected_udp_socket("127.0.0.1", 9999)
+ await udp.send(b"x")
+ with fail_after(1):
+ await udp.aclose()
+
async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None:
async with await create_connected_udp_socket(
"localhost", 5000, family=family
From 57cb4a92861992b6ce120979a4acc8b4148f551e Mon Sep 17 00:00:00 2001
From: Guflly <145608489+Guflly@users.noreply.github.com>
Date: Mon, 27 Jul 2026 14:07:27 -0700
Subject: [PATCH 2/4] Fix forced UDP socket closure
---
src/anyio/_backends/_asyncio.py | 14 ++++++++++----
tests/test_sockets.py | 6 +++++-
2 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py
index 9b91f5472..8e2bbddd0 100644
--- a/src/anyio/_backends/_asyncio.py
+++ b/src/anyio/_backends/_asyncio.py
@@ -1687,8 +1687,11 @@ async def aclose(self) -> None:
self._closed = True
if not self._transport.is_closing():
self._transport.close()
- await sleep(0)
- self._transport.abort()
+ try:
+ await sleep(0)
+ finally:
+ self._transport.abort()
+ await AsyncIOBackend.cancel_shielded_checkpoint()
await self._protocol.closed_event.wait()
@@ -1739,8 +1742,11 @@ async def aclose(self) -> None:
self._closed = True
if not self._transport.is_closing():
self._transport.close()
- await sleep(0)
- self._transport.abort()
+ try:
+ await sleep(0)
+ finally:
+ self._transport.abort()
+ await AsyncIOBackend.cancel_shielded_checkpoint()
await self._protocol.closed_event.wait()
diff --git a/tests/test_sockets.py b/tests/test_sockets.py
index 40c5ede6c..64a3fab28 100644
--- a/tests/test_sockets.py
+++ b/tests/test_sockets.py
@@ -34,6 +34,7 @@
from pytest import FixtureRequest
from pytest_mock.plugin import MockerFixture
+import anyio
from anyio import (
BrokenResourceError,
BusyResourceError,
@@ -1747,9 +1748,12 @@ async def test_aclose_waits_for_fd_release(
@pytest.mark.parametrize("anyio_backend", asyncio_params)
async def test_aclose_during_send(self) -> None:
udp = await create_udp_socket(local_host="127.0.0.1")
+ sock = udp.extra(SocketAttribute.raw_socket)
await udp.sendto(b"x", "127.0.0.1", 9999)
with fail_after(1):
- await udp.aclose()
+ await anyio.aclose_forcefully(udp)
+
+ assert sock.fileno() == -1
async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None:
async with await create_udp_socket(
From 51e137ebd28ebc09c2ccd1e0d7814df0f68d352a Mon Sep 17 00:00:00 2001
From: maz
Date: Fri, 31 Jul 2026 14:20:04 -0700
Subject: [PATCH 3/4] Fix forced TCP socket closure
---
src/anyio/_backends/_asyncio.py | 7 +++++--
tests/test_sockets.py | 10 ++++++++++
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py
index 8e2bbddd0..3745f99ce 100644
--- a/src/anyio/_backends/_asyncio.py
+++ b/src/anyio/_backends/_asyncio.py
@@ -1403,8 +1403,11 @@ async def aclose(self) -> None:
pass
self._transport.close()
- await sleep(0)
- self._transport.abort()
+ try:
+ await sleep(0)
+ finally:
+ self._transport.abort()
+ await AsyncIOBackend.cancel_shielded_checkpoint()
class _RawSocketMixin:
diff --git a/tests/test_sockets.py b/tests/test_sockets.py
index 64a3fab28..5c06602f9 100644
--- a/tests/test_sockets.py
+++ b/tests/test_sockets.py
@@ -519,6 +519,16 @@ async def test_send_after_close(self, server_addr: tuple[str, int]) -> None:
with pytest.raises(ClosedResourceError):
await stream.send(b"foo")
+ @pytest.mark.parametrize("anyio_backend", asyncio_params)
+ async def test_aclose_forcefully(
+ self, server_addr: tuple[str, int]
+ ) -> None:
+ stream = await connect_tcp(*server_addr)
+ sock = stream.extra(SocketAttribute.raw_socket)
+ await stream.send(b"x")
+ await anyio.aclose_forcefully(stream)
+ assert sock.fileno() == -1
+
async def test_receive_after_peer_closed(
self, family: AnyIPAddressFamily, request: FixtureRequest
) -> None:
From 7928b000c7b762fd820fb6aeecf58e0fc15a64a8 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Fri, 31 Jul 2026 21:20:29 +0000
Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
tests/test_sockets.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/tests/test_sockets.py b/tests/test_sockets.py
index 5c06602f9..dbdb7d31f 100644
--- a/tests/test_sockets.py
+++ b/tests/test_sockets.py
@@ -520,9 +520,7 @@ async def test_send_after_close(self, server_addr: tuple[str, int]) -> None:
await stream.send(b"foo")
@pytest.mark.parametrize("anyio_backend", asyncio_params)
- async def test_aclose_forcefully(
- self, server_addr: tuple[str, int]
- ) -> None:
+ async def test_aclose_forcefully(self, server_addr: tuple[str, int]) -> None:
stream = await connect_tcp(*server_addr)
sock = stream.extra(SocketAttribute.raw_socket)
await stream.send(b"x")