The stackoverflow exception (fixed in #32) doesn't occur anymore, but it still doesn't seem to work correctly after a connection loss under Android. I have also tested it on Windows and there are no problems. I guess ofc Android has a different System.Net.Socket-Implementation, so there might be a different behaviour.
This is easily reproducable using the latest code on an Android Device or emulator: You just have to disconnect Wifi or LAN and reconnect and Sisk is unable to recover, so the webserver isn't reachable after reconnect.
So I added some debug statements to StartAccept and ProcessAcceptInline in HttpHost.cs like this:
HttpCosts.cs Debug-Code
[MethodImpl ( MethodImplOptions.AggressiveInlining )]
private void StartAccept ( int poolIndex ) {
if (!_isListening)
return;
while (_isListening) {
var args = _acceptArgsPool [ poolIndex ];
args.AcceptSocket = null;
try {
if (_listener.AcceptAsync ( args )) {
Debug.WriteLine ( $"[StartAccept] poolIndex={poolIndex} pending async" );
return;
}
Debug.WriteLine ( $"[StartAccept] poolIndex={poolIndex} completed synchronously" );
}
catch (ObjectDisposedException) {
Debug.WriteLine ( $"[StartAccept] poolIndex={poolIndex} ObjectDisposedException – listener disposed, exiting" );
return;
}
catch (SocketException ex) {
Debug.WriteLine ( $"[StartAccept] poolIndex={poolIndex} SocketException={ex.SocketErrorCode} – queuing retry in {ListenerAcceptRetryDelayMilliseconds}ms" );
QueueStartAccept ( poolIndex, ListenerAcceptRetryDelayMilliseconds );
return;
}
int rearmDelayMs = ProcessAcceptInline ( args, poolIndex );
if (rearmDelayMs > 0) {
Debug.WriteLine ( $"[StartAccept] poolIndex={poolIndex} rearm delay={rearmDelayMs}ms – queuing" );
QueueStartAccept ( poolIndex, rearmDelayMs );
return;
}
}
Debug.WriteLine ( $"[StartAccept] poolIndex={poolIndex} loop exited – _isListening=false" );
}
[MethodImpl ( MethodImplOptions.AggressiveOptimization )]
private int ProcessAcceptInline ( SocketAsyncEventArgs e, int poolIndex ) {
Debug.WriteLine ( $"[ProcessAcceptInline] poolIndex={poolIndex} SocketError={e.SocketError} AcceptSocket={e.AcceptSocket?.RemoteEndPoint?.ToString () ?? "null"}" );
if (e.SocketError != SocketError.Success || e.AcceptSocket is null) {
var socketError = e.SocketError;
e.AcceptSocket?.Dispose ();
e.AcceptSocket = null;
bool isNoise = IsConnectionAcceptNoise ( socketError );
int delay = isNoise ? 0 : ListenerAcceptRetryDelayMilliseconds;
Debug.WriteLine ( $"[ProcessAcceptInline] poolIndex={poolIndex} error path – isNoise={isNoise} delay={delay}ms" );
return delay;
}
Socket client = e.AcceptSocket;
e.AcceptSocket = null;
Debug.WriteLine ( $"[ProcessAcceptInline] poolIndex={poolIndex} accepted client remote={client.RemoteEndPoint} local={client.LocalEndPoint}" );
var workItem = new ConnectionWorkItem { Host = this, Socket = client };
ThreadPool.UnsafeQueueUserWorkItem ( workItem, preferLocal: false );
Debug.WriteLine ( $"[ProcessAcceptInline] poolIndex={poolIndex} work item queued" );
return 0;
}
Notice in this test I have set AcceptPoolSize to one for debugging purposes thats why you will only see poolIndex = 0 in the debug output!
HttpCosts.cs Debug-Output
Working connection
[ProcessAcceptInline] poolIndex=0 SocketError=Success AcceptSocket=192.168.20.192:35442
[ProcessAcceptInline] poolIndex=0 accepted client remote=192.168.20.192:35442 local=192.168.20.192:4646
[ProcessAcceptInline] poolIndex=0 work item queued
[StartAccept] poolIndex=0 pending async
--------
LAN cable removed from the Android Box
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 completed synchronously
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 completed synchronously
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[StartAccept] poolIndex=0 completed synchronously
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[StartAccept] poolIndex=0 completed synchronously
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[StartAccept] poolIndex=0 completed synchronously
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[StartAccept] poolIndex=0 completed synchronously
[...]
------
LAN cable reconnected to the box
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[StartAccept] poolIndex=0 completed synchronously
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[StartAccept] poolIndex=0 completed synchronously
[StartAccept] poolIndex=0 completed synchronously
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[StartAccept] poolIndex=0 rearm delay=250ms – queuing[StartAccept] poolIndex=0 completed synchronously
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[StartAccept] poolIndex=0 completed synchronously
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[StartAccept] poolIndex=0 completed synchronously
[ProcessAcceptInline] poolIndex=0 SocketError=InvalidArgument AcceptSocket=null
[ProcessAcceptInline] poolIndex=0 error path – isNoise=False delay=250ms
[StartAccept] poolIndex=0 rearm delay=250ms – queuing
[...]
Conclusion
From the logs, the listener repeatedly receives SocketError.InvalidArgument after a network disconnect – likely because the underlying socket becomes invalid when the network interface is reset. The listener then retries with a 250 ms delay indefinitely, but never recovers, even after the network reconnects.
The root cause appears to be that the listener socket itself dies on network interface reset (observed on Android) and is never rebuilt – only the accept loop is retried on the same invalid socket.
Hence, I asked claude to modify HttpHost.cs with the ability to do a rebuild of the listener in this case. I don't really know if this is the right solution to the problem but it works:
HttpHost.cs with the ability to rebuild the listener
using System.Diagnostics;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using Sisk.Cadente.HttpSerializer;
namespace Sisk.Cadente;
/// <summary>
/// Represents an HTTP host that listens for incoming TCP connections and handles HTTP requests.
/// </summary>
public sealed class HttpHost : IDisposable {
private readonly IPEndPoint _endpoint;
private Socket _listener; // no longer readonly – rebuilt on fatal errors
// cache line padding to reduce false sharing
private volatile bool _disposedValue;
private volatile bool _isListening;
private readonly SocketAsyncEventArgs [] _acceptArgsPool;
private readonly int [] _acceptArgsAvailable;
private int _listenerRestarting = 0; // Interlocked flag – only one restart at a time
private const int AcceptPoolSize = 8;
private const int ListenerAcceptRetryDelayMilliseconds = 250;
/// <summary>
/// Gets or sets the name of the server in the header name.
/// </summary>
public static string ServerNameHeader { get; set; } = "Sisk";
/// <summary>
/// Gets the endpoint of the <see cref="HttpHost"/>.
/// </summary>
public IPEndPoint Endpoint => _endpoint;
/// <summary>
/// Gets or sets an <see cref="HttpHostHandler"/> instance for this <see cref="HttpHost"/>.
/// </summary>
public HttpHostHandler? Handler { get; set; }
/// <summary>
/// Gets a value indicating whether this <see cref="HttpHost"/> has been disposed.
/// </summary>
public bool IsDisposed => _disposedValue;
/// <summary>
/// Gets or sets the HTTPS options for secure connections. Setting an <see cref="Sisk.Cadente.HttpsOptions"/> object in this
/// property, the <see cref="Sisk.Cadente.HttpHost"/> will use HTTPS instead of HTTP.
/// </summary>
public HttpsOptions? HttpsOptions { get; set; }
/// <summary>
/// Gets the <see cref="HttpHostTimeoutManager"/> for this <see cref="HttpHost"/>.
/// </summary>
public HttpHostTimeoutManager TimeoutManager { get; } = new HttpHostTimeoutManager ();
/// <summary>
/// Initializes a new instance of the <see cref="HttpHost"/> class using the specified <see cref="IPEndPoint"/>.
/// </summary>
/// <param name="endpoint">The <see cref="IPEndPoint"/> to listen on.</param>
public HttpHost ( IPEndPoint endpoint ) {
_endpoint = endpoint;
_listener = CreateListenerSocket ();
_acceptArgsPool = new SocketAsyncEventArgs [ AcceptPoolSize ];
_acceptArgsAvailable = new int [ AcceptPoolSize ];
for (int i = 0; i < AcceptPoolSize; i++) {
var args = new SocketAsyncEventArgs ();
args.Completed += OnAcceptCompleted;
args.UserToken = i;
_acceptArgsPool [ i ] = args;
_acceptArgsAvailable [ i ] = 1;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpHost"/> class using the specified port on the loopback address.
/// </summary>
/// <param name="port">The port number to listen on.</param>
public HttpHost ( int port ) : this ( new IPEndPoint ( IPAddress.Loopback, port ) ) { }
/// <summary>
/// Starts the HTTP host and begins listening for incoming connections.
/// </summary>
public void Start () {
if (_isListening)
return;
ObjectDisposedException.ThrowIf ( _disposedValue, this );
_listener.Bind ( _endpoint );
_listener.Listen ( backlog: 4096 );
_isListening = true;
for (int i = 0; i < AcceptPoolSize; i++) {
StartAccept ( i );
}
}
// Creates and configures a fresh listener socket.
private Socket CreateListenerSocket () {
var socket = new Socket ( _endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp );
socket.NoDelay = true;
socket.LingerState = new LingerOption ( false, 0 );
socket.ReceiveBufferSize = 128 * 1024;
socket.SendBufferSize = 128 * 1024;
if (socket.AddressFamily == AddressFamily.InterNetworkV6 && _endpoint.Address.Equals ( IPAddress.IPv6Any )) {
socket.DualMode = true;
}
socket.SetSocketOption ( SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true );
socket.SetSocketOption ( SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true );
socket.SetSocketOption ( SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 3 );
socket.SetSocketOption ( SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 300 );
socket.SetSocketOption ( SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 3 );
return socket;
}
[MethodImpl ( MethodImplOptions.AggressiveInlining )]
private void StartAccept ( int poolIndex ) {
if (!_isListening)
return;
// Do not call AcceptAsync while a listener rebuild is in progress.
if (_listenerRestarting == 1)
return;
while (_isListening) {
var args = _acceptArgsPool [ poolIndex ];
args.AcceptSocket = null;
try {
if (_listener.AcceptAsync ( args ))
return;
}
catch (ObjectDisposedException) {
return;
}
catch (SocketException) {
QueueStartAccept ( poolIndex, ListenerAcceptRetryDelayMilliseconds );
return;
}
int rearmDelayMs = ProcessAcceptInline ( args, poolIndex );
if (rearmDelayMs < 0) {
// Fatal error – listener rebuild triggered; this pool slot stops here.
return;
}
if (rearmDelayMs > 0) {
QueueStartAccept ( poolIndex, rearmDelayMs );
return;
}
}
}
private void OnAcceptCompleted ( object? sender, SocketAsyncEventArgs e ) {
int poolIndex = (int) e.UserToken!;
int rearmDelayMs = ProcessAcceptInline ( e, poolIndex );
if (rearmDelayMs < 0) {
// Fatal error – listener rebuild triggered; this pool slot stops here.
return;
}
if (rearmDelayMs > 0)
QueueStartAccept ( poolIndex, rearmDelayMs );
else
StartAccept ( poolIndex );
}
[MethodImpl ( MethodImplOptions.AggressiveOptimization )]
private int ProcessAcceptInline ( SocketAsyncEventArgs e, int poolIndex ) {
if (e.SocketError != SocketError.Success || e.AcceptSocket is null) {
var socketError = e.SocketError;
e.AcceptSocket?.Dispose ();
e.AcceptSocket = null;
if (IsListenerFatalError ( socketError )) {
Debug.WriteLine ( $"[ProcessAcceptInline] poolIndex={poolIndex} fatal error={socketError} – triggering listener rebuild" );
TriggerListenerRebuild ();
return -1; // signals callers to stop this pool slot
}
return IsConnectionAcceptNoise ( socketError )
? 0
: ListenerAcceptRetryDelayMilliseconds;
}
Socket client = e.AcceptSocket;
e.AcceptSocket = null;
var workItem = new ConnectionWorkItem { Host = this, Socket = client };
ThreadPool.UnsafeQueueUserWorkItem ( workItem, preferLocal: false );
return 0;
}
// Returns true for errors that mean the listener socket itself is dead,
// as opposed to a transient per-connection error.
private static bool IsListenerFatalError ( SocketError error ) => error is
SocketError.InvalidArgument or // network interface recycled (common on Android)
SocketError.NotSocket or // socket was disposed externally
SocketError.Shutdown or
SocketError.OperationAborted or
SocketError.Interrupted;
// Ensures only one rebuild task is running at any time.
private void TriggerListenerRebuild () {
if (Interlocked.CompareExchange ( ref _listenerRestarting, 1, 0 ) != 0) {
Debug.WriteLine ( "[TriggerListenerRebuild] rebuild already in progress – skipping" );
return;
}
_ = RebuildListenerAsync ();
}
private async Task RebuildListenerAsync () {
Debug.WriteLine ( "[RebuildListenerAsync] starting listener rebuild..." );
// Tear down the broken socket.
try { _listener.Close (); } catch { }
try { _listener.Dispose (); } catch { }
int attempt = 0;
while (_isListening && !_disposedValue) {
attempt++;
await Task.Delay ( ListenerAcceptRetryDelayMilliseconds ).ConfigureAwait ( false );
try {
var newSocket = CreateListenerSocket ();
newSocket.Bind ( _endpoint );
newSocket.Listen ( backlog: 4096 );
_listener = newSocket;
Debug.WriteLine ( $"[RebuildListenerAsync] listener rebuilt successfully on attempt #{attempt}" );
break;
}
catch (SocketException ex) {
Debug.WriteLine ( $"[RebuildListenerAsync] attempt #{attempt} failed: {ex.SocketErrorCode} – retrying..." );
}
}
// Release the flag before restarting accept pools so that
// StartAccept can call AcceptAsync on the new socket.
Interlocked.Exchange ( ref _listenerRestarting, 0 );
if (_isListening && !_disposedValue) {
Debug.WriteLine ( "[RebuildListenerAsync] restarting all accept pool slots" );
for (int i = 0; i < AcceptPoolSize; i++)
StartAccept ( i );
}
}
private void QueueStartAccept ( int poolIndex, int delayMs = 0 ) {
if (!_isListening)
return;
if (delayMs <= 0) {
ThreadPool.UnsafeQueueUserWorkItem (
static state => state.Host.StartAccept ( state.PoolIndex ),
(Host: this, PoolIndex: poolIndex),
preferLocal: false );
return;
}
_ = QueueStartAcceptAsync ( poolIndex, delayMs );
}
private async Task QueueStartAcceptAsync ( int poolIndex, int delayMs ) {
await Task.Delay ( delayMs ).ConfigureAwait ( false );
if (_isListening)
StartAccept ( poolIndex );
}
private static bool IsConnectionAcceptNoise ( SocketError socketError ) =>
socketError is SocketError.Success
or SocketError.ConnectionReset
or SocketError.ConnectionAborted
or SocketError.NetworkReset;
[MethodImpl ( MethodImplOptions.AggressiveOptimization )]
internal async Task ProcessConnectionCoreAsync ( Socket client ) {
// Early exit if there is no handler
if (Handler is null) {
client.Dispose ();
return;
}
Logger.LogInformation ( $"Connection started from {client.RemoteEndPoint} on {client.LocalEndPoint}" );
int readTimeoutMs = (int) TimeoutManager.ClientReadTimeout.TotalMilliseconds;
int writeTimeoutMs = (int) TimeoutManager.ClientWriteTimeout.TotalMilliseconds;
client.ReceiveTimeout = readTimeoutMs;
client.SendTimeout = writeTimeoutMs;
client.NoDelay = true;
NetworkStream clientStream = new ( client, ownsSocket: true );
clientStream.ReadTimeout = readTimeoutMs;
clientStream.WriteTimeout = writeTimeoutMs;
Stream connectionStream;
SslStream? sslStream = null;
try {
if (HttpsOptions is not null) {
Logger.LogInformation ( $"Starting SSL handshake" );
sslStream = new SslStream ( clientStream, leaveInnerStreamOpen: false );
connectionStream = sslStream;
using var handshakeCts = new CancellationTokenSource ( TimeoutManager.SslHandshakeTimeout );
try {
await sslStream.AuthenticateAsServerAsync ( new SslServerAuthenticationOptions {
ServerCertificate = HttpsOptions.ServerCertificate,
ClientCertificateRequired = HttpsOptions.ClientCertificateRequired,
EnabledSslProtocols = HttpsOptions.AllowedProtocols,
CertificateRevocationCheckMode = HttpsOptions.CheckCertificateRevocation
? System.Security.Cryptography.X509Certificates.X509RevocationMode.Online
: System.Security.Cryptography.X509Certificates.X509RevocationMode.NoCheck
}, handshakeCts.Token ).ConfigureAwait ( false );
Logger.LogInformation ( $"SSL handshake successfull" );
}
catch (Exception ex) {
Logger.LogInformation ( $"Failed SSL handshake: {ex.Message}" );
await WriteHandshakeErrorAsync ( clientStream ).ConfigureAwait ( false );
return;
}
}
else {
connectionStream = clientStream;
}
IPEndPoint clientEndpoint = (IPEndPoint) client.RemoteEndPoint!;
HttpHostClient hostClient = new ( clientEndpoint, CancellationToken.None );
if (sslStream is not null) {
hostClient.IsSecureConnection = true;
hostClient.ClientCertificate = sslStream.RemoteCertificate;
}
await using HttpConnection connection = new ( hostClient, connectionStream, this, clientEndpoint );
Logger.LogInformation ( $"call OnClientConnectedAsync" );
await Handler.OnClientConnectedAsync ( this, hostClient ).ConfigureAwait ( false );
try {
Logger.LogInformation ( $"call HandleConnectionEventsAsync" );
await connection.HandleConnectionEventsAsync ( default ).ConfigureAwait ( false );
}
catch (Exception ex) {
Logger.LogInformation ( $"HandleConnectionEventsAsync/exception: {ex}" );
}
finally {
Logger.LogInformation ( $"call OnClientDisconnectedAsync" );
await Handler.OnClientDisconnectedAsync ( this, hostClient ).ConfigureAwait ( false );
}
}
catch (SocketException sex) {
Logger.LogInformation ( $"SocketException: {sex.Message}" );
}
catch (IOException iox) {
Logger.LogInformation ( $"IOException: {iox.Message}" );
}
catch (Exception ex) {
Logger.LogInformation ( $"Exception: {ex.Message}" );
}
finally {
Logger.LogInformation ( $"cleanup" );
if (sslStream is not null)
await sslStream.DisposeAsync ().ConfigureAwait ( false );
else
await clientStream.DisposeAsync ().ConfigureAwait ( false );
}
}
[MethodImpl ( MethodImplOptions.NoInlining )]
private static async Task WriteHandshakeErrorAsync ( Stream stream ) {
byte [] message = HttpResponseSerializer.GetRawMessage ( "SSL/TLS Handshake failed.", 400, "Bad Request" );
try {
await stream.WriteAsync ( message ).ConfigureAwait ( false );
}
catch { }
}
[MethodImpl ( MethodImplOptions.AggressiveInlining )]
internal ValueTask InvokeContextCreated ( HttpHostContext context ) {
if (_disposedValue || Handler is null)
return ValueTask.CompletedTask;
return new ValueTask ( Handler.OnContextCreatedAsync ( this, context ) );
}
/// <summary>
/// Stops the HTTP host from listening for incoming HTTP requests.
/// </summary>
public void Stop () {
if (!_isListening)
return;
_isListening = false;
try { _listener.Close (); } catch { }
}
private void Dispose ( bool disposing ) {
if (_disposedValue)
return;
_disposedValue = true;
if (disposing) {
_isListening = false;
try { _listener.Close (); } catch { }
try { _listener.Dispose (); } catch { }
for (int i = 0; i < AcceptPoolSize; i++) {
try { _acceptArgsPool [ i ].Dispose (); } catch { }
}
}
}
/// <inheritdoc/>
public void Dispose () {
Dispose ( true );
GC.SuppressFinalize ( this );
}
/// <inheritdoc/>
~HttpHost () {
Dispose ( false );
}
}
The attached code performs a complete listener rebuild when a fatal socket error is detected (e.g. SocketError.InvalidArgument, see IsListenerFatalError). Consider it a proof of concept rather than a production-ready fix, which is why no PR was opened.
The core question is: is there a proper solution for this Android-specific behavior where the listener socket becomes permanently invalid after a network interface reset?
The stackoverflow exception (fixed in #32) doesn't occur anymore, but it still doesn't seem to work correctly after a connection loss under Android. I have also tested it on Windows and there are no problems. I guess ofc Android has a different System.Net.Socket-Implementation, so there might be a different behaviour.
This is easily reproducable using the latest code on an Android Device or emulator: You just have to disconnect Wifi or LAN and reconnect and Sisk is unable to recover, so the webserver isn't reachable after reconnect.
So I added some debug statements to
StartAcceptandProcessAcceptInlinein HttpHost.cs like this:HttpCosts.cs Debug-Code
Notice in this test I have set
AcceptPoolSizeto one for debugging purposes thats why you will only see poolIndex = 0 in the debug output!HttpCosts.cs Debug-Output
Conclusion
From the logs, the listener repeatedly receives
SocketError.InvalidArgumentafter a network disconnect – likely because the underlying socket becomes invalid when the network interface is reset. The listener then retries with a 250 ms delay indefinitely, but never recovers, even after the network reconnects.The root cause appears to be that the listener socket itself dies on network interface reset (observed on Android) and is never rebuilt – only the accept loop is retried on the same invalid socket.
Hence, I asked claude to modify
HttpHost.cswith the ability to do a rebuild of the listener in this case. I don't really know if this is the right solution to the problem but it works:HttpHost.cs with the ability to rebuild the listener
The attached code performs a complete listener rebuild when a fatal socket error is detected (e.g.
SocketError.InvalidArgument, seeIsListenerFatalError). Consider it a proof of concept rather than a production-ready fix, which is why no PR was opened.The core question is: is there a proper solution for this Android-specific behavior where the listener socket becomes permanently invalid after a network interface reset?