Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Desktop] WebSocketException after listening 90+ seconds with .Net Framework, and its ok with .net core 2.0 #27192

Closed
kiddoneal opened this issue Aug 22, 2018 · 7 comments
Assignees
Labels
area-System.Net bug tenet-compatibility Incompatibility with previous versions or .NET Framework
Milestone

Comments

@kiddoneal
Copy link

Exception in ReceiveAsync in .NET Framework 4.7(4.7.1/4.7.2), Works in .NET core2.0 (2.1).
and its ok with the WebSocket4Net.dll in .NET Framework 4.7
reproduce:

  1. connect to wss://ws.gate.io/v3/
  2. wait for 90+seconeds, and it will reproduced

here is the log:

2018-08-20 14:06:22.425 [ERR] [18] [Gateio Reader WEBSOCKET COMMUNICATOR] Listen ReceiveAsync WebSocketException : System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'SslStream'.
   at System.Net.Security.SslState.CheckThrow(Boolean authSuccessCheck, Boolean shutdownCheck)
   at System.Net.Security.SslState.get_SecureStream()
   at System.Net.TlsStream.EndRead(IAsyncResult asyncResult)
   at System.Threading.Tasks.TaskFactory`1.FromAsyncTrimPromise`1.Complete(TInstance thisRef, Func`3 endMethod, IAsyncResult asyncResult, Boolean requiresSynchronization)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.WebSockets.WebSocketConnectionStream.<ReadAsync>d__21.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.WebSockets.WebSocketBase.WebSocketOperation.<Process>d__19.MoveNext()
   at System.Net.WebSockets.WebSocketBase.WebSocketOperation.<Process>d__19.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.WebSockets.WebSocketBase.<ReceiveAsyncCore>d__45.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at CryptoExchange.Net.Implementation.WebsocketCommunicator.<Listen>d__74.MoveNext().

my code looks like this :

{
    var _client = new ClientWebSocket()
            {
                Options = { KeepAliveInterval = new TimeSpan(0, 0, 5)}
            });
    _client.ConnectAsync(new Uri("wss://ws.gate.io/v3/"), token);
    while (Active && !token.IsCancellationRequested)
            {
                using (var ms = new MemoryStream())
                {
                    do
                    {
                        try
                        {
                            data = await _client.ReceiveAsync(buffer, token);
                        }
                        catch (TaskCanceledException e)
                        {
                            Log.Error(L($"Listen ReceiveAsync TaskCanceledException : {e}."));
                            return;
                        }
                        catch (WebSocketException e)
                        {
                            Log.Error(L($"Listen ReceiveAsync WebSocketException : {e}."));
                            await Task.Delay(ReconnectingTimeoutMs);
                            await Reconnect();
                            return;
                        }
                        catch (Exception e)
                        {
                            Log.Error(L($"Listen ReceiveAsync exception : {e}."));
                            await Task.Delay(ReconnectingTimeoutMs);
                            await Reconnect();
                            return;
                        }
                        if (data == null)
                        {
                            Log.Error(L($"Listen ReceiveAsync no data received."));
                            continue;
                        }
                        ms.Write(buffer.Array, buffer.Offset, data.Count);
                    }
                    while (!data.EndOfMessage);
                }
            }
}
@karelz
Copy link
Member

karelz commented Aug 22, 2018

Do you have minimal repro we can try locally? (i.e. is the code above complete?)

@rdlaitila
Copy link

I'm experiencing the same issue. c# ClientWebSocket will time out 90-100 seconds even when keepalives and data is moving across the wire. I'll try to work up a sharable sample if possible, but I found one possible explanation:

https://stackoverflow.com/questions/40502921/net-websockets-forcibly-closed-despite-keep-alive-and-activity-on-the-connectio

as indicated from the stackoverflow article setting the service point manager max idle value does appear to keep my ClientWebSocket connection alive:

ServicePointManager.MaxServicePointIdleTime = int.MaxValue;

Here is a sample stack from my ClientWebSocket when not using the above workaround:

System.Net.WebSockets.WebSocketException: The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'SslStream'.
    at System.Net.Security.SslState.CheckThrow(Boolean authSuccessCheck, Boolean shutdownCheck)
   at System.Net.Security.SslState.get_SecureStream()
   at System.Net.TlsStream.EndRead(IAsyncResult asyncResult)
   at System.Net.PooledStream.EndRead(IAsyncResult asyncResult)
   at System.IO.Stream.<>c.<BeginEndReadAsync>b__43_1(Stream stream, IAsyncResult asyncResult)
   at System.Threading.Tasks.TaskFactory`1.FromAsyncTrimPromise`1.Complete(TInstance thisRef, Func`3 endMethod, IAsyncResult asyncResult, Boolean requiresSynchronization)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.WebSockets.WebSocketConnectionStream.<ReadAsync>d__21.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
   at System.Net.WebSockets.WebSocketBase.WebSocketOperation.<Process>d__19.MoveNext()
 --- End of inner exception stack trace ---
    at System.Net.WebSockets.WebSocketBase.WebSocketOperation.<Process>d__19.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.WebSockets.WebSocketBase.<ReceiveAsyncCore>d__45.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at [OMITTED].<NextAsync>d__8.MoveNext() in C:\[OMITTED].cs:line 125

@karelz
Copy link
Member

karelz commented Aug 23, 2018

@rdlaitila do you have a minimal repro that you could share with us?

@rdlaitila
Copy link

@karelz you can find a minimal repro solution here https://github.com/rdlaitila/corefx-issue-31880-repro

Instructions

  • Debug run the Server project
  • Debug run the Client project
  • notice the client program should throw an exception at around 1m 40s
  • you may also browse https://localhost:44361/wwwroot/browser-test.html and note that browser based websocket clients have no issue and do not time out after 1m 40s

I've also included workarounds in the client and server projects that may be helpful in determining the issue.

With the prior stackoverflow comments and some behavior notes, it would seem ClientWebSocket is timing out the low level socket due to the owin pipeline returning a Content-Length: 0 with the websocket upgrade response. Most other websocket clients must just ignore this response header. The server workaround flag in the Server project simply writes Content-Length: 1 and the C# ClientWebSocket never times out.

I am running the repro solution under Windows 10. Please let me know if you need any additional info from me. Thanks!

@kiddoneal
Copy link
Author

kiddoneal commented Aug 24, 2018

@karelz here is the minimal repro, Thanks!

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace QD.BTC.Arbi.TestDemo
{
    class TestGateio_ReproSslException
    {
        static void Main(string[] args)
        {
            Run();
            Console.ReadLine();
        }

        private static async void Run()
        {
            var client = new ClientWebSocket()
            {
                Options = { KeepAliveInterval = new TimeSpan(0, 0, 5) }
            };
            var uri = new Uri("wss://ws.gate.io/v3/");
            var _cancelation = new CancellationTokenSource();
            await client.ConnectAsync(uri, _cancelation.Token);

            var message = "{\"id\":52997,\"method\":\"depth.subscribe\",\"params\":[[\"ETH_USDT\",5,\"0\"]]}";
            var buffer = Encoding.UTF8.GetBytes(message);
            var messageSegment = new ArraySegment<byte>(buffer);
            await client.SendAsync(messageSegment, WebSocketMessageType.Text, true, _cancelation.Token);

            ArraySegment<Byte> bufferRecv = new ArraySegment<byte>(new Byte[8192]);

            while (true)
            {
                try
                {
                    var data = await client.ReceiveAsync(bufferRecv, _cancelation.Token);
                    Console.WriteLine(data.ToString());
                }
                catch (TaskCanceledException e)
                {
                    Console.WriteLine(($"Listen ReceiveAsync TaskCanceledException : {e}."));
                    return;
                }
                catch (WebSocketException e)
                {
                    Console.WriteLine(($"Listen ReceiveAsync WebSocketException : {e}."));

                    return;
                }
                catch (Exception e)
                {
                    Console.WriteLine(($"Listen ReceiveAsync exception : {e}."));

                    return;
                }
            }
        }
    }
}

@caesar-chen
Copy link
Contributor

Seems this is .NET Framework related issue. I'm only able to repro in .NET Framework, but I will let @kiddoneal @rdlaitila to confirm.

Both repros provided are targeting .NET Framework, and in the issue description:

Exception in ReceiveAsync in .NET Framework 4.7(4.7.1/4.7.2), Works in .NET core2.0 (2.1).

Initial analysis (on Framework code base):

Like answered in StackOverflow question, this is caused by server sends back 101 response with Content-Length: 0.

  1. When we receive a response with Content-Length == 0, and after finished reading from the connection, we will remove the reservation on the request, and perform a CheckIdle() on the Connection.
  2. If there is no outstanding requests in the WaitList or WriteList at the moment, CheckIdle() will return true, mark the Connection as Idle and remove it from the ServicePoint.
  3. When the ServicePoint doesn't have any active Connection, the m_ExpiringTimer (default to 100 seconds) will be started.
  4. After timer expires, the non-active Connection will be closed, resulting the WebSocketException.

Thoughts:

Server should never send Content-Length header in 1xx response (RFC 7230 Section 3.3.2). As a client, we can better handle this by adding logging for this scenario to help developer trouble-shooting the issue.

Additional information:

Response from wss://ws.gate.io/v3/

HTTP/1.1 101 Switching Protocols
Server: openresty/1.13.6.1
Date: Thu, 06 Sep 2018 17:22:18 GMT
-> Content-Length: 0
Connection: upgrade
Sec-WebSocket-Accept: SQz0btULByIplLgVeCObEzEKeBg=
Upgrade: websocket
EndTime: 10:22:24.120
ReceivedBytes: 717
SentBytes: 75

RFC 7230:

A server MUST NOT send a Content-Length header field in any response
with a status code of 1xx (Informational) or 204 (No Content).

@dotnet/ncl @karelz

@karelz karelz changed the title WebSocketException after listening 90+ seconds with .Net Framework, and its ok with .net core 2.0 [Desktop] WebSocketException after listening 90+ seconds with .Net Framework, and its ok with .net core 2.0 Sep 6, 2018
@karelz
Copy link
Member

karelz commented Sep 6, 2018

Given that it is report against .NET Framework (which we do not track on GitHub - see repo main page) and given that primary problem is non-compliant server, we will close it here.
If there is evidence that such non-compliant servers are more common in the wild, we can consider adjusting .NET Framework in future versions - follow the steps for providing .NET Framework feedback in such case.

@karelz karelz closed this as completed Sep 6, 2018
@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 3.0 milestone Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 15, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Net bug tenet-compatibility Incompatibility with previous versions or .NET Framework
Projects
None yet
Development

No branches or pull requests

5 participants