agronholm / agronholm/anyio

`BrokenResourceError` is the wrong error for a temporary Windows UDP reset

Open
#1,239 3 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Python
Stars
2.5k
Forks
260
Avg merge
1d 8h
Merged PRs (30d)
17

Description

### Things to check first

- [x] I have searched the existing issues and didn't find my bug already reported there

- [x] I have checked that my bug is still present in the latest release

### AnyIO version

4.14.2

### Python version

3.14

### What happened?

On Windows, sending a UDP datagram to an address with nothing bound to it can cause a
later receive on the same socket to observe `WSAECONNRESET` (`WinError 10054`).

however after this error

- the socket still works
- a later `receive()` can still time out normally
- once a real peer starts sending, the same socket can still receive datagrams

That means this is a temporary per-datagram condition, not a terminal resource
failure. Raising `anyio.BrokenResourceError` is therefore misleading, because
`BrokenResourceError` implies the resource is no longer usable.

### How can we reproduce the bug?

```python
import socket

import anyio
from anyio.abc import SocketAttribute

async def main() -> None:
errors = []
async with await anyio.create_udp_socket(
family=socket.AF_INET, local_host="127.0.0.1"
) as udp:
# An address on loopback with nothing bound to it.
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
probe.bind(("127.0.0.1", 0))
dead_host, dead_port = probe.getsockname()
probe.close()

# Provokes an ICMP port-unreachable back to `udp` on Windows.
await udp.sendto(b"ping", dead_host, dead_port)

# Windows + trio: raises anyio.BrokenResourceError (WinError 10054),
# even though `udp` is still perfectly usable.
# Windows + asyncio: no error; this times out waiting for real data.
try:
with anyio.fail_after(2):
await udp.receive()
except TimeoutError:
print("timeout out correctly 1")
except anyio.BrokenResourceError as e:
errors.append(e)
else:
errors.append(AssertionError("did not timeout"))

try:
with anyio.fail_after(2):
await udp.receive()
except TimeoutError:
print("timeout out correctly 2")
except anyio.BrokenResourceError as e:
errors.append(e)
else:
errors.append(AssertionError("did not timeout"))

local_host, local_port = udp.extra(SocketAttribute.local_address)

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
server.bind((dead_host, dead_port))
server.sendto(b"pong", (local_host, local_port))
finally:
server.close()

try:
data, addr = await udp.receive()
except Exception as e:
errors.append(e)
else:
print(f"recieved {data}")
if data != b"pong":
errors.append(AssertionError(f"unexpected payload: {data!r}"))
if addr != (dead_host, dead_port):
errors.append(AssertionError(f"unexpected peer: {addr!r}"))

if errors:
raise ExceptionGroup("socket errors", errors)

anyio.run(main, backend="trio") # vs. backend="asyncio"
```

on windows prints:

```
timeout out correctly 2
recieved b'pong'
```

then raises the BrokenResourceError in an ExceptionGroup

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.