#5242·libuv

UDP: ICMP errors are reported to the recv callback on Unix but silently discarded on Windows

Author: graingertCreated Aug 26, 2026Updated Aug 30, 2026
  • Version: v1.x (f87c8e4); the behaviour long predates it
  • Platform: Windows (any), contrasted with Linux

What happens

When a UDP datagram elicits an ICMP port unreachable, the resulting error reaches the receive callback on Unix, but is silently discarded on Windows.

src/win/udp.c treats WSAECONNRESET/WSAENETRESET on a receive as "not a real error" in both the overlapped-completion path and the non-blocking path:

c
        } else if (err == WSAECONNRESET || err == WSAENETRESET) {
          /* WSAECONNRESET/WSANETRESET is ignored because this just indicates
           * that a previous sendto operation failed.
           */
          handle->recv_cb(handle, 0, &buf, NULL, 0);
        }

Since nread == 0 with addr == NULL is documented as "there is nothing to read", the application cannot distinguish this from an ordinary spurious wakeup, and the error is lost. On Unix the same situation is delivered as nread < 0 (UV_ECONNREFUSED).

Why it matters

The information is available from the OS: CPython's own IOCP-based asyncio proactor surfaces this exact condition as ERROR_PORT_UNREACHABLE (winerror 1234) and hands it to the application. So this is a libuv policy choice rather than a platform limitation.

It surfaces as a cross-platform behaviour split for libuv bindings. Concretely, in AnyIO (Python) a task blocked in receive() on a connected UDP socket is woken with an error on Linux (under uvloop as well as the stdlib backends) and on Windows under the stdlib proactor event loop, but under winloop (libuv on Windows) it is never woken at all, because the protocol's error_received() is never called. Context: https://github.com/agronholm/anyio/pull/1295

Prior art

  • #1449 raised the same symptom in 2017 and was closed on the grounds that the docs describe nread == 0 && addr == NULL as "nothing to read". The swallow itself dates back to joyent/libuv#1426.
  • UV_UDP_LINUX_RECVERR (flag 32) was added in #2872 so applications can opt into fuller ICMP error reporting on Linux, which suggests the project considers this information worth surfacing when it is asked for.
  • docs/src/udp.rst already documents the uv_udp_bind flag under the platform-neutral name UV_UDP_RECVERR, even though the enum is UV_UDP_LINUX_RECVERR.

Suggested resolution

Extend the existing opt-in to Windows rather than changing the default:

  1. Add UV_UDP_RECVERR = 32 as a platform-neutral name, keeping UV_UDP_LINUX_RECVERR as an alias with the same value, so nothing existing breaks.
  2. On Windows, when a handle was bound with that flag, deliver WSAECONNRESET/ WSAENETRESET to the receive callback as a negative nread with UV_UDP_RECVERR in flags (mirroring what uv__udp_recvmsg_errqueue already does on Linux), without calling uv_udp_recv_stop() — unlike a genuine receive error the socket stays usable, and Unix does not stop reading either.

Failing that, documenting the difference in docs/src/udp.rst would at least let binding authors discover it from the docs rather than from a hanging test.

I have a patch for the above ready to go (include/uv.h, src/uv-common.h, src/win/udp.c, docs, and enabling the existing can_recverr path in test/test-udp-send-unreachable.c on Windows) and will open it as a PR if the approach looks acceptable. The full test suite passes on Linux.

One caveat in the interest of full disclosure: I do not have a Windows machine, so the Windows code path in that patch is reviewed but not compiled or run — the behaviour described above was observed through winloop in CI and traced back to this code. I have not written a standalone C reproducer either; happy to add one if that would help.