Bug Report: Unhandled SocketException on Proactor I/O Thread Causes Process Termination
Package
LogicielNetMQ v4.0.0 (forked from NetMQ)
Environment
•
.NET 8 / .NET 10 (client app), .NET Standard 2.0 (communication library)
•
Windows, x64
•
Sockets used: subscriberSocket, dealerSocket with NetMQPoller
•
ZMQ heartbeat enabled (HeartBeatInterval + HeartbeatTimeout + TcpKeepalive)
---
Summary
When all network packets to the remote server are dropped (simulated using Clumsy network blocker tool), the application process terminates with an unhandled System.Net.Sockets.SocketException thrown on NetMQ's internal Proactor I/O thread. Because the Proactor runs on a raw System.Threading.Thread, the exception cannot be caught or suppressed by application code — in .NET Core/.NET 5+, AppDomain.CurrentDomain.UnhandledException is notification-only and cannot prevent process termination.
---
Full Stack Trace
System.Net.Sockets.SocketException
HResult=0x80004005
Message=An existing connection was forcibly closed by the remote host.
at NetMQ.Core.Mailbox.Send(Command cmd) -- Mailbox.cs:line 36
at NetMQ.Core.SocketBase.TrySend(Msg& msg, TimeSpan timeout, Boolean more) -- SocketBase.cs:line 641
at NetMQ.Core.MonitorEvent.Write(SocketBase s) -- MonitorEvent.cs:line 141
at NetMQ.Core.SocketBase.EventDisconnected(String addr, AsyncSocket ch) -- SocketBase.cs:line 1075
at NetMQ.Core.Transports.StreamEngine.Error() -- StreamEngine.cs:line 268
at NetMQ.Core.Transports.StreamEngine.ProcessInput() -- StreamEngine.cs:line 782
at NetMQ.Core.Transports.StreamEngine.FeedAction(Action action, SocketError socketError, Int32 bytesTransferred) -- StreamEngine.cs:line 283
at NetMQ.Core.Utils.Proactor.Loop() -- Proactor.cs:line 129
at System.Threading.Thread.StartHelper.Callback(Object state)
---
Root Cause Analysis
---
System.Net.Sockets.SocketException
HResult=0x80004005
Message=An existing connection was forcibly closed by the remote host.
at NetMQ.Core.Mailbox.Send(Command cmd) -- Mailbox.cs:line 36
at NetMQ.Core.SocketBase.TrySend(Msg& msg, TimeSpan timeout, Boolean more) -- SocketBase.cs:line 641
at NetMQ.Core.MonitorEvent.Write(SocketBase s) -- MonitorEvent.cs:line 141
at NetMQ.Core.SocketBase.EventDisconnected(String addr, AsyncSocket ch) -- SocketBase.cs:line 1075
at NetMQ.Core.Transports.StreamEngine.Error() -- StreamEngine.cs:line 268
at NetMQ.Core.Transports.StreamEngine.ProcessInput() -- StreamEngine.cs:line 782
at NetMQ.Core.Transports.StreamEngine.FeedAction(Action action, SocketError socketError, Int32 bytesTransferred) -- StreamEngine.cs:line 283
at NetMQ.Core.Utils.Proactor.Loop() -- Proactor.cs:line 129
at System.Threading.Thread.StartHelper.Callback(Object state)
---
Step-by-step:
1. Proactor.Loop() receives an I/O completion with SocketError != Success (remote connection dropped).
2. StreamEngine.FeedAction() detects the error and calls StreamEngine.Error().
3. Error() calls SocketBase.EventDisconnected() on the monitored socket.
4. EventDisconnected() checks (m_monitorEvents & SocketEvents.Disconnected) != 0 — if a NetMQMonitor was attached, this is true.
5. It calls MonitorEvent.Write(m_monitorSocket) which does m_monitorSocket.TrySend(ref msg, ...).
6. Inside TrySend, the internal Mailbox.Send(Command cmd) writes a command and calls m_signaler.Send().
7. The Signaler uses a pair of localhost TCP sockets (127.0.0.1) for inter-thread notification. Under heavy network disruption (e.g., Clumsy blocking all packets), this Socket.Send() call also throws SocketException.
8. This exception is unhandled on the Proactor thread — Proactor.Loop() has no try/catch for SocketException around the completion handler invocation.
9. In .NET Core, an unhandled exception on a raw Thread terminates the process. AppDomain.UnhandledException fires but cannot prevent termination.
Please help me catch this Exception or handle it gracefully thanks.