Skip to content

Add WebSocketOptions.Timeout to ClientWebSocket #48729

Closed
@bjornen77

Description

@bjornen77

Edited by @CarnaViire

Justification

This API allows the users to identify and close idle WebSocket connections without having to rely on the TCP KeepAlive timeout, which can be way too big (e.g. ~15 min on Linux), and can be impossible or very hard to configure.

// see the original issue in the end of the post

API Proposal

namespace System.Net.WebSockets;

// used to create a base WebSocket (ManagedWebSocket) instance
public class WebSocketCreationOptions
{
    // existing
    //public TimeSpan KeepAliveInterval { get; set; } // default: infinite (off)

    // new
    public TimeSpan KeepAliveTimeout { get; set; } = Timeout.InfiniteTimeSpan;

    // ...
}

// used to configure a ClientWebSocket instance
public class ClientWebSocketOptions
{
    // existing
    //[UnsupportedOSPlatform("browser")]
    //public TimeSpan KeepAliveInterval { get; set; } // default: 30s

    // new
    [UnsupportedOSPlatform("browser")]
    public TimeSpan KeepAliveTimeout { get; set; } = Timeout.InfiniteTimeSpan;

    // ...
}

Usage

For ClientWebSocket:

var ws = new ClientWebSocket();
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
await ws.ConnectAsync(uri, cts.Token);

For ManagedWebSocket:

var options = new WebSocketCreationOptions()
{
    KeepAliveInterval = WebSocket.DefaultKeepAliveInterval,
    KeepAliveTimeout = TimeSpan.FromSeconds(10)
};
var ws = WebSocket.CreateFromStream(stream, options);

Notes on behavior

Timeout.InfiniteTimeSpan or TimeSpan.Zero would disable the timeout logic and fall back to the existing "unsolicited" Pong keep-alive (per understanding in #35473 (comment) we cannot send Pings without waiting for the reply)

Any other positive timeout value would turn on Ping-Pong logic; we will be sending Pings instead of Pongs and track whether we've received Pongs in time; if not, the connection is treated as stale and aborted. The behavior and implementation should be similar to HTTP/2 KeepAlivePingTimeout.

Alternative Naming

We could name the new property KeepAlivePingTimeout. This will "break" it's similarity to KeepAliveInterval, but it might help highlight that Pings, not Pongs, are being sent (if we care enough about this distinction).

namespace System.Net.WebSockets;

public class WebSocketCreationOptions
{
    //public TimeSpan KeepAliveInterval { get; set; } // existing
    public TimeSpan KeepAlivePingTimeout { get; set; } // new

    // ...
}

public class ClientWebSocketOptions
{
    //public TimeSpan KeepAliveInterval { get; set; } // existing
    public TimeSpan KeepAlivePingTimeout { get; set; } // new

    // ...
}

Existing API shape for other keep-alives

For comparison:

// HTTP/2 in runtime:
namespace System.Net.Http;
public class SocketsHttpHandler
{
    public TimeSpan KeepAlivePingDelay { get; set; } // default: infinite (off)
    public TimeSpan KeepAlivePingTimeout { get; set; } // default: 20s (N/A when delay=inf)
    public HttpKeepAlivePingPolicy KeepAlivePingPolicy { get; set; } // default: Always
    // ...
}

// HTTP/2 in Kestrel:
namespace Microsoft.AspNetCore.Server.Kestrel.Core;
public class Http2Limits
{
    public TimeSpan KeepAlivePingDelay { get; set; } // default: infinite (off)
    public TimeSpan KeepAlivePingTimeout { get; set; } // default: 20s (N/A when delay=inf)
    // ...
}

// Sockets in runtime:
namespace System.Net.Sockets;
public enum SocketOptionName
{
    TcpKeepAliveInterval, // between probe retries, if no answer received
    TcpKeepAliveRetryCount, // probe retries before deemed idle, if no answer received
    TcpKeepAliveTime, // before the first probe, after the last received byte
    // ...
}

Original issue by @bjornen77

I think that adding a WebSocketOptions.Timeout property would be a good thing.

Today, the ClientWebSocket is closed when the TCP Retransmission times out(if the connection to the server drops for some reason). This timeout seems to be different on Windows and Linux. By default, on Windows it takes about 21 seconds and on Linux it takes about 15 minutes before the WebSocket is closed. It would be good if we could have a separate Timeout setting, so that we get the same behavior regardless of the platform. And also that this could be configured by the application without having to modify the TCP Timeout settings in the OS... The websocket should still be disconnected if the OS TCP Timeout is reached.

The ClientWebSocket today only sends "Pong" to the server, and therefore it does not wait for a "Pong" response to arrive. I think that the ClientWebSocket shall send a "Ping" instead, and then verify that a "Pong" is received within the Timeout set. If not received, the connection shall be closed. Or at least it should expose the "received "Pong" message in the WebSocketReceiveResult.MessageType. It would be good if an received "Ping" message from a server is also exposed. Then the application could use these Ping/Pong message types to implement its own timeout logic.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Netin-prThere is an active PR which will close this issue when it is merged

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions