Repository: sanic-org/sanic

Issue:
Ability to set ping_interval and ping_timeout parameters for WebSocketCommonProtocol
**Is your feature request related to a problem? Please describe.**
The `ping_interval` and `ping_timeout` parameters for [WebSocketCommonProtocol](https://websockets.readthedocs.io/en/stable/api.html#websockets.protocol.WebSocketCommonProtocol) default to 20 seconds. It would be nice to be able to set them with configuration values.

**Describe the solution you'd like**
Add `WEBSOCKET_PING_INTERVAL` and `WEBSOCKET_PING_TIMEOUT` to configuration values and pass the values into the [WebSocketCommonProtocol initialization](https://github.com/huge-success/sanic/blob/master/sanic/websocket.py#L116).

**Additional context**




Discussion:

Resolving patch:
diff --git a/sanic/config.py b/sanic/config.py
--- a/sanic/config.py
+++ b/sanic/config.py
@@ -24,6 +24,8 @@
     "WEBSOCKET_MAX_QUEUE": 32,
     "WEBSOCKET_READ_LIMIT": 2 ** 16,
     "WEBSOCKET_WRITE_LIMIT": 2 ** 16,
+    "WEBSOCKET_PING_TIMEOUT": 20,
+    "WEBSOCKET_PING_INTERVAL": 20,
     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,  # 15 sec
     "ACCESS_LOG": True,
     "FORWARDED_SECRET": None,
diff --git a/sanic/server.py b/sanic/server.py
--- a/sanic/server.py
+++ b/sanic/server.py
@@ -14,11 +14,13 @@
 from signal import SIG_IGN, SIGINT, SIGTERM, Signals
 from signal import signal as signal_func
 from time import time
+from typing import Type
 
 from httptools import HttpRequestParser  # type: ignore
 from httptools.parser.errors import HttpParserError  # type: ignore
 
 from sanic.compat import Header, ctrlc_workaround_for_windows
+from sanic.config import Config
 from sanic.exceptions import (
     HeaderExpectationFailed,
     InvalidUsage,
@@ -844,6 +846,7 @@ def serve(
     app.asgi = False
 
     connections = connections if connections is not None else set()
+    protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
     server = partial(
         protocol,
         loop=loop,
@@ -852,6 +855,7 @@ def serve(
         app=app,
         state=state,
         unix=unix,
+        **protocol_kwargs,
     )
     asyncio_server_kwargs = (
         asyncio_server_kwargs if asyncio_server_kwargs else {}
@@ -948,6 +952,21 @@ def serve(
         remove_unix_socket(unix)
 
 
+def _build_protocol_kwargs(
+    protocol: Type[HttpProtocol], config: Config
+) -> dict:
+    if hasattr(protocol, "websocket_timeout"):
+        return {
+            "max_size": config.WEBSOCKET_MAX_SIZE,
+            "max_queue": config.WEBSOCKET_MAX_QUEUE,
+            "read_limit": config.WEBSOCKET_READ_LIMIT,
+            "write_limit": config.WEBSOCKET_WRITE_LIMIT,
+            "ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
+            "ping_interval": config.WEBSOCKET_PING_INTERVAL,
+        }
+    return {}
+
+
 def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
     """Create TCP server socket.
     :param host: IPv4, IPv6 or hostname may be specified
diff --git a/sanic/websocket.py b/sanic/websocket.py
--- a/sanic/websocket.py
+++ b/sanic/websocket.py
@@ -35,6 +35,8 @@ def __init__(
         websocket_max_queue=None,
         websocket_read_limit=2 ** 16,
         websocket_write_limit=2 ** 16,
+        websocket_ping_interval=20,
+        websocket_ping_timeout=20,
         **kwargs
     ):
         super().__init__(*args, **kwargs)
@@ -45,6 +47,8 @@ def __init__(
         self.websocket_max_queue = websocket_max_queue
         self.websocket_read_limit = websocket_read_limit
         self.websocket_write_limit = websocket_write_limit
+        self.websocket_ping_interval = websocket_ping_interval
+        self.websocket_ping_timeout = websocket_ping_timeout
 
     # timeouts make no sense for websocket routes
     def request_timeout_callback(self):
@@ -119,6 +123,8 @@ async def websocket_handshake(self, request, subprotocols=None):
             max_queue=self.websocket_max_queue,
             read_limit=self.websocket_read_limit,
             write_limit=self.websocket_write_limit,
+            ping_interval=self.websocket_ping_interval,
+            ping_timeout=self.websocket_ping_timeout,
         )
         # Following two lines are required for websockets 8.x
         self.websocket.is_client = False


Test patch:
diff --git a/tests/test_app.py b/tests/test_app.py
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -1,6 +1,7 @@
 import asyncio
 import logging
 import sys
+from unittest.mock import patch
 
 from inspect import isawaitable
 
@@ -148,6 +149,35 @@ async def handler(request, ws):
     assert app.websocket_enabled == True
 
 
+@patch("sanic.app.WebSocketProtocol")
+def test_app_websocket_parameters(websocket_protocol_mock, app):
+    app.config.WEBSOCKET_MAX_SIZE = 44
+    app.config.WEBSOCKET_MAX_QUEUE = 45
+    app.config.WEBSOCKET_READ_LIMIT = 46
+    app.config.WEBSOCKET_WRITE_LIMIT = 47
+    app.config.WEBSOCKET_PING_TIMEOUT = 48
+    app.config.WEBSOCKET_PING_INTERVAL = 50
+
+    @app.websocket("/ws")
+    async def handler(request, ws):
+        await ws.send("test")
+
+    try:
+        # This will fail because WebSocketProtocol is mocked and only the call kwargs matter
+        app.test_client.get("/ws")
+    except:
+        pass
+
+    websocket_protocol_call_args = websocket_protocol_mock.call_args
+    ws_kwargs = websocket_protocol_call_args[1]
+    assert ws_kwargs["max_size"] == app.config.WEBSOCKET_MAX_SIZE
+    assert ws_kwargs["max_queue"] == app.config.WEBSOCKET_MAX_QUEUE
+    assert ws_kwargs["read_limit"] == app.config.WEBSOCKET_READ_LIMIT
+    assert ws_kwargs["write_limit"] == app.config.WEBSOCKET_WRITE_LIMIT
+    assert ws_kwargs["ping_timeout"] == app.config.WEBSOCKET_PING_TIMEOUT
+    assert ws_kwargs["ping_interval"] == app.config.WEBSOCKET_PING_INTERVAL
+
+
 def test_handle_request_with_nested_exception(app, monkeypatch):
 
     err_msg = "Mock Exception"