New Upstream Snapshot - websocket-client

Ready changes

Summary

Merged new upstream version: 1.4.2+git20230113.1.601a902 (was: 1.2.3).

Resulting package

Built on 2023-01-19T14:08 (took 5m58s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots python3-websocket

Lintian Result

Diff

diff --git a/ChangeLog b/ChangeLog
index 6789489..409041a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,44 @@
 ChangeLog
 ============
 
+- 1.4.2
+  - create_dispatcher is determined by URL ws/wss, NOT by presence of sslopt args, to maintain consistency (#875)
+  - Remove redundant key generation line (#864)
+  - Updated docs to fix old links and updated CI to include Python 3.11
+
+- 1.4.1
+  - Fix stack growth bug when `run_forever` reconnects (#854)
+  - Add doctest CI for sphinx docs code examples (d150099)
+  - General docs improvements
+
+- 1.4.0
+  - Fix automatic reconnect with `run_forever` (#838)
+  - Allow a timeout to be set when using a proxy (#842)
+
+- 1.3.3
+  - Fix unclosed socket error (#826)
+  - Update header dict access (#818)
+  - Add utf8 workaround to docs (fc9ee9f)
+
+- 1.3.2
+  - Add support for pre-initialized stream socket in new WebSocketApp (#804)
+  - Remove rel.saferead() in examples (f0bf03d)
+  - Increase scope of linting checks (dca4022)
+  - Start adding type hints (a8a4099)
+
+- 1.3.1
+  - Fix 10 year old bug and improve dispatcher handling for run_forever (#795)
+  - Fix run_forever to never return None, only return True or False, and add two tests (#788)
+  - Remove Python 3.6 support, EOL in Dec 2021
+
+- 1.3.0
+  - BREAKING: Set Origin header to use https:// scheme when wss:// WebSocket URL is passed (#787)
+  - Replace deprecated/broken WebSocket URLs with working ones (6ad5197)
+  - Add documentation referencing rel for automatic reconnection with run_forever()
+  - Add missing opcodes 1012, 1013 (#771)
+  - Add errno.ENETUNREACH to improve error handling (da1b050)
+  - Minor documentation improvements and typo fixes
+
 - 1.2.3
   - Fix broken run_forever() functionality (#769)
 
diff --git a/LICENSE b/LICENSE
index 27269dd..a5db00e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright 2021 engn33r
+   Copyright 2022 engn33r
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/PKG-INFO b/PKG-INFO
index 116110c..b0b7e1d 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,35 +1,34 @@
 Metadata-Version: 2.1
 Name: websocket-client
-Version: 1.2.3
+Version: 1.4.2
 Summary: WebSocket client for Python with low level API options
 Home-page: https://github.com/websocket-client/websocket-client.git
+Download-URL: https://github.com/websocket-client/websocket-client/releases
 Author: liris
 Author-email: liris.pp@gmail.com
 License: Apache-2.0
-Download-URL: https://github.com/websocket-client/websocket-client/releases
 Project-URL: Documentation, https://websocket-client.readthedocs.io/
 Project-URL: Source, https://github.com/websocket-client/websocket-client/
 Keywords: websockets client
-Platform: UNKNOWN
 Classifier: Development Status :: 4 - Beta
 Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
 Classifier: Operating System :: MacOS :: MacOS X
 Classifier: Operating System :: POSIX
 Classifier: Operating System :: Microsoft :: Windows
 Classifier: Topic :: Internet
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Classifier: Intended Audience :: Developers
-Requires-Python: >=3.6
+Requires-Python: >=3.7
 Description-Content-Type: text/markdown
-Provides-Extra: test
-Provides-Extra: optional
 Provides-Extra: docs
+Provides-Extra: optional
+Provides-Extra: test
 License-File: LICENSE
 
 [![docs](https://readthedocs.org/projects/websocket-client/badge/?style=flat)](https://websocket-client.readthedocs.io/)
@@ -59,7 +58,7 @@ Please see the [contribution guidelines](https://github.com/websocket-client/web
 ## Installation
 
 You can use either `python3 setup.py install` or `pip3 install websocket-client`
-to install. This module is tested on Python 3.6+.
+to install. This module is tested on Python 3.7+.
 
 There are several optional dependencies that can be installed to enable
 specific websocket-client features.
@@ -70,6 +69,9 @@ specific websocket-client features.
 - To install `Sphinx` and `sphinx_rtd_theme` to build project documentation, use:
  `pip3 install websocket-client[docs]`
 
+While not a strict dependency, [rel](https://github.com/bubbleboy14/registeredeventlistener)
+is useful when using `run_forever` with automatic reconnect. Install rel with `pip3 install rel`.
+
 Footnote: Some shells, such as zsh, require you to escape the `[` and `]` characters with a `\`.
 
 ## Usage Tips
@@ -102,13 +104,28 @@ Many more examples are found in the
 ### Long-lived Connection
 
 Most real-world WebSockets situations involve longer-lived connections.
-The WebSocketApp `run_forever` loop automatically tries to reconnect when a
-connection is lost, and provides a variety of event-based connection controls.
+The WebSocketApp `run_forever` loop will automatically try to reconnect
+to an open WebSocket connection when a network
+connection is lost if it is provided with:
+
+- a `dispatcher` argument (async dispatcher like rel or pyevent)
+- a non-zero `reconnect` argument (delay between disconnection and attempted reconnection)
+
+`run_forever` provides a variety of event-based connection controls
+using callbacks like `on_message` and `on_error`.
+`run_forever` does not automatically reconnect if the server
+closes the WebSocket gracefully (returning
+[a standard websocket close code](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1)).
+Customizing behavior when the server closes
+the WebSocket should be handled in the `on_close` callback.
+This example uses [rel](https://github.com/bubbleboy14/registeredeventlistener)
+for the dispatcher to provide automatic reconnection.
 
 ```python
 import websocket
 import _thread
 import time
+import rel
 
 def on_message(ws, message):
     print(message)
@@ -120,24 +137,19 @@ def on_close(ws, close_status_code, close_msg):
     print("### closed ###")
 
 def on_open(ws):
-    def run(*args):
-        for i in range(3):
-            time.sleep(1)
-            ws.send("Hello %d" % i)
-        time.sleep(1)
-        ws.close()
-        print("thread terminating...")
-    _thread.start_new_thread(run, ())
+    print("Opened connection")
 
 if __name__ == "__main__":
     websocket.enableTrace(True)
-    ws = websocket.WebSocketApp("ws://echo.websocket.org/",
+    ws = websocket.WebSocketApp("wss://api.gemini.com/v1/marketdata/BTCUSD",
                               on_open=on_open,
                               on_message=on_message,
                               on_error=on_error,
                               on_close=on_close)
 
-    ws.run_forever()
+    ws.run_forever(dispatcher=rel, reconnect=5)  # Set dispatcher to automatic reconnection, 5 second reconnect delay if connection closed unexpectedly
+    rel.signal(2, rel.abort)  # Keyboard Interrupt
+    rel.dispatch()
 ```
 
 ### Short-lived Connection
@@ -148,7 +160,9 @@ server is running and responds properly to a specific request.
 
 ```python
 from websocket import create_connection
-ws = create_connection("ws://echo.websocket.org/")
+
+ws = create_connection("ws://echo.websocket.events/")
+print(ws.recv())
 print("Sending 'Hello, World'...")
 ws.send("Hello, World")
 print("Sent")
@@ -157,18 +171,3 @@ result =  ws.recv()
 print("Received '%s'" % result)
 ws.close()
 ```
-
-If you want to customize socket options, set sockopt, as seen below:
-
-```python
-from websocket import create_connection
-ws = create_connection("ws://echo.websocket.org/",
-                        sockopt=((socket.IPPROTO_TCP, socket.TCP_NODELAY),))
-```
-
-### Acknowledgements
-
-Thanks to @battlemidget and @ralphbean for helping migrate this project to
-Python 3.
-
-
diff --git a/README.md b/README.md
index 04c2bd9..d5c9bc5 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Please see the [contribution guidelines](https://github.com/websocket-client/web
 ## Installation
 
 You can use either `python3 setup.py install` or `pip3 install websocket-client`
-to install. This module is tested on Python 3.6+.
+to install. This module is tested on Python 3.7+.
 
 There are several optional dependencies that can be installed to enable
 specific websocket-client features.
@@ -36,6 +36,9 @@ specific websocket-client features.
 - To install `Sphinx` and `sphinx_rtd_theme` to build project documentation, use:
  `pip3 install websocket-client[docs]`
 
+While not a strict dependency, [rel](https://github.com/bubbleboy14/registeredeventlistener)
+is useful when using `run_forever` with automatic reconnect. Install rel with `pip3 install rel`.
+
 Footnote: Some shells, such as zsh, require you to escape the `[` and `]` characters with a `\`.
 
 ## Usage Tips
@@ -68,13 +71,28 @@ Many more examples are found in the
 ### Long-lived Connection
 
 Most real-world WebSockets situations involve longer-lived connections.
-The WebSocketApp `run_forever` loop automatically tries to reconnect when a
-connection is lost, and provides a variety of event-based connection controls.
+The WebSocketApp `run_forever` loop will automatically try to reconnect
+to an open WebSocket connection when a network
+connection is lost if it is provided with:
+
+- a `dispatcher` argument (async dispatcher like rel or pyevent)
+- a non-zero `reconnect` argument (delay between disconnection and attempted reconnection)
+
+`run_forever` provides a variety of event-based connection controls
+using callbacks like `on_message` and `on_error`.
+`run_forever` does not automatically reconnect if the server
+closes the WebSocket gracefully (returning
+[a standard websocket close code](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1)).
+Customizing behavior when the server closes
+the WebSocket should be handled in the `on_close` callback.
+This example uses [rel](https://github.com/bubbleboy14/registeredeventlistener)
+for the dispatcher to provide automatic reconnection.
 
 ```python
 import websocket
 import _thread
 import time
+import rel
 
 def on_message(ws, message):
     print(message)
@@ -86,24 +104,19 @@ def on_close(ws, close_status_code, close_msg):
     print("### closed ###")
 
 def on_open(ws):
-    def run(*args):
-        for i in range(3):
-            time.sleep(1)
-            ws.send("Hello %d" % i)
-        time.sleep(1)
-        ws.close()
-        print("thread terminating...")
-    _thread.start_new_thread(run, ())
+    print("Opened connection")
 
 if __name__ == "__main__":
     websocket.enableTrace(True)
-    ws = websocket.WebSocketApp("ws://echo.websocket.org/",
+    ws = websocket.WebSocketApp("wss://api.gemini.com/v1/marketdata/BTCUSD",
                               on_open=on_open,
                               on_message=on_message,
                               on_error=on_error,
                               on_close=on_close)
 
-    ws.run_forever()
+    ws.run_forever(dispatcher=rel, reconnect=5)  # Set dispatcher to automatic reconnection, 5 second reconnect delay if connection closed unexpectedly
+    rel.signal(2, rel.abort)  # Keyboard Interrupt
+    rel.dispatch()
 ```
 
 ### Short-lived Connection
@@ -114,7 +127,9 @@ server is running and responds properly to a specific request.
 
 ```python
 from websocket import create_connection
-ws = create_connection("ws://echo.websocket.org/")
+
+ws = create_connection("ws://echo.websocket.events/")
+print(ws.recv())
 print("Sending 'Hello, World'...")
 ws.send("Hello, World")
 print("Sent")
@@ -123,16 +138,3 @@ result =  ws.recv()
 print("Received '%s'" % result)
 ws.close()
 ```
-
-If you want to customize socket options, set sockopt, as seen below:
-
-```python
-from websocket import create_connection
-ws = create_connection("ws://echo.websocket.org/",
-                        sockopt=((socket.IPPROTO_TCP, socket.TCP_NODELAY),))
-```
-
-### Acknowledgements
-
-Thanks to @battlemidget and @ralphbean for helping migrate this project to
-Python 3.
diff --git a/debian/changelog b/debian/changelog
index 71ec60d..d2dcfbb 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+websocket-client (1.4.2+git20230113.1.601a902-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 19 Jan 2023 14:04:05 -0000
+
 websocket-client (1.2.3-1) unstable; urgency=medium
 
   * New upstream release.
diff --git a/examples/echo_client.py b/examples/echo_client.py
index d4fa3b7..359ec38 100644
--- a/examples/echo_client.py
+++ b/examples/echo_client.py
@@ -2,7 +2,8 @@ import websocket
 
 if __name__ == "__main__":
     websocket.enableTrace(True)
-    ws = websocket.create_connection("ws://echo.websocket.org/")
+    ws = websocket.create_connection("ws://echo.websocket.events/")
+    ws.recv()
     print("Sending 'Hello, World'...")
     ws.send("Hello, World")
     print("Sent")
diff --git a/examples/echoapp_client.py b/examples/echoapp_client.py
index bf70731..0d2b2f5 100644
--- a/examples/echoapp_client.py
+++ b/examples/echoapp_client.py
@@ -35,7 +35,7 @@ def on_open(ws):
 if __name__ == "__main__":
     websocket.enableTrace(True)
     if len(sys.argv) < 2:
-        host = "ws://echo.websocket.org/"
+        host = "ws://echo.websocket.events/"
     else:
         host = sys.argv[1]
     ws = websocket.WebSocketApp(host,
diff --git a/examples/rel_client.py b/examples/rel_client.py
new file mode 100644
index 0000000..9b9c7da
--- /dev/null
+++ b/examples/rel_client.py
@@ -0,0 +1,11 @@
+import websocket
+import rel
+
+addr = "wss://api.gemini.com/v1/marketdata/%s"
+
+if __name__ == "__main__":
+    for symbol in ["BTCUSD", "ETHUSD", "ETHBTC"]:
+        ws = websocket.WebSocketApp(addr % (symbol,), on_message=lambda w, m: print(m))
+        ws.run_forever(dispatcher=rel)
+    rel.signal(2, rel.abort)  # Keyboard Interrupt
+    rel.dispatch()
diff --git a/setup.py b/setup.py
index 6e034b0..cb14d1c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,12 +1,12 @@
-"""
-
-"""
+import sys
+import pkg_resources
+from setuptools import setup, find_packages
 
 """
 setup.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,12 +20,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 """
-import sys
-
-from setuptools import setup, find_packages
-import pkg_resources
 
-VERSION = "1.2.3"
+VERSION = "1.4.2"
 
 install_requires = []
 tests_require = []
@@ -41,7 +37,7 @@ setup(
     license="Apache-2.0",
     url="https://github.com/websocket-client/websocket-client.git",
     download_url='https://github.com/websocket-client/websocket-client/releases',
-    python_requires='>=3.6',
+    python_requires='>=3.7',
     extras_require={
         "test": ["websockets"],
         "optional": ["python-socks", "wsaccel"],
@@ -51,11 +47,11 @@ setup(
         "Development Status :: 4 - Beta",
         "License :: OSI Approved :: Apache Software License",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
         "Operating System :: MacOS :: MacOS X",
         "Operating System :: POSIX",
         "Operating System :: Microsoft :: Windows",
diff --git a/websocket/__init__.py b/websocket/__init__.py
index 05aae2b..588a8f2 100644
--- a/websocket/__init__.py
+++ b/websocket/__init__.py
@@ -2,7 +2,7 @@
 __init__.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and
 limitations under the License.
 """
 from ._abnf import *
-from ._app import WebSocketApp
+from ._app import WebSocketApp, setReconnect
 from ._core import *
 from ._exceptions import *
 from ._logging import *
 from ._socket import *
 
-__version__ = "1.2.3"
+__version__ = "1.4.2"
diff --git a/websocket/_abnf.py b/websocket/_abnf.py
index e9909ff..2e5ad97 100644
--- a/websocket/_abnf.py
+++ b/websocket/_abnf.py
@@ -1,12 +1,17 @@
-"""
+import array
+import os
+import struct
+import sys
 
-"""
+from ._exceptions import *
+from ._utils import validate_utf8
+from threading import Lock
 
 """
 _abnf.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,14 +25,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 """
-import array
-import os
-import struct
-import sys
-
-from ._exceptions import *
-from ._utils import validate_utf8
-from threading import Lock
 
 try:
     # If wsaccel is available, use compiled routines to mask data.
@@ -79,6 +76,8 @@ STATUS_POLICY_VIOLATION = 1008
 STATUS_MESSAGE_TOO_BIG = 1009
 STATUS_INVALID_EXTENSION = 1010
 STATUS_UNEXPECTED_CONDITION = 1011
+STATUS_SERVICE_RESTART = 1012
+STATUS_TRY_AGAIN_LATER = 1013
 STATUS_BAD_GATEWAY = 1014
 STATUS_TLS_HANDSHAKE_ERROR = 1015
 
@@ -92,6 +91,8 @@ VALID_CLOSE_STATUS = (
     STATUS_MESSAGE_TOO_BIG,
     STATUS_INVALID_EXTENSION,
     STATUS_UNEXPECTED_CONDITION,
+    STATUS_SERVICE_RESTART,
+    STATUS_TRY_AGAIN_LATER,
     STATUS_BAD_GATEWAY,
 )
 
@@ -146,7 +147,7 @@ class ABNF:
         self.data = data
         self.get_mask_key = os.urandom
 
-    def validate(self, skip_utf8_validation=False):
+    def validate(self, skip_utf8_validation=False) -> None:
         """
         Validate the ABNF frame.
 
@@ -174,13 +175,13 @@ class ABNF:
 
             code = 256 * self.data[0] + self.data[1]
             if not self._is_valid_close_status(code):
-                raise WebSocketProtocolException("Invalid close opcode.")
+                raise WebSocketProtocolException("Invalid close opcode %r", code)
 
     @staticmethod
-    def _is_valid_close_status(code):
+    def _is_valid_close_status(code: int) -> bool:
         return code in VALID_CLOSE_STATUS or (3000 <= code < 5000)
 
-    def __str__(self):
+    def __str__(self) -> str:
         return "fin=" + str(self.fin) \
             + " opcode=" + str(self.opcode) \
             + " data=" + str(self.data)
@@ -206,7 +207,7 @@ class ABNF:
         # mask must be set if send data from client
         return ABNF(fin, 0, 0, 0, opcode, 1, data)
 
-    def format(self):
+    def format(self) -> bytes:
         """
         Format this object to string(byte array) to send data to server.
         """
@@ -251,9 +252,9 @@ class ABNF:
 
         Parameters
         ----------
-        mask_key: <type>
-            4 byte string.
-        data: <type>
+        mask_key: bytes or str
+            4 byte mask.
+        data: bytes or str
             data to mask/unmask.
         """
         if data is None:
@@ -286,7 +287,7 @@ class frame_buffer:
         self.length = None
         self.mask = None
 
-    def has_received_header(self):
+    def has_received_header(self) -> bool:
         return self.header is None
 
     def recv_header(self):
@@ -308,7 +309,7 @@ class frame_buffer:
             return False
         return self.header[frame_buffer._HEADER_MASK_INDEX]
 
-    def has_received_length(self):
+    def has_received_length(self) -> bool:
         return self.length is None
 
     def recv_length(self):
@@ -323,7 +324,7 @@ class frame_buffer:
         else:
             self.length = length_bits
 
-    def has_received_mask(self):
+    def has_received_mask(self) -> bool:
         return self.mask is None
 
     def recv_mask(self):
@@ -360,7 +361,7 @@ class frame_buffer:
 
         return frame
 
-    def recv_strict(self, bufsize):
+    def recv_strict(self, bufsize: int) -> bytes:
         shortage = bufsize - sum(map(len, self.recv_buffer))
         while shortage > 0:
             # Limit buffer size that we pass to socket.recv() to avoid
diff --git a/websocket/_app.py b/websocket/_app.py
index 1afd3d2..e5a4484 100644
--- a/websocket/_app.py
+++ b/websocket/_app.py
@@ -1,12 +1,20 @@
-"""
-
-"""
+import inspect
+import selectors
+import sys
+import threading
+import time
+import traceback
+from ._abnf import ABNF
+from ._url import parse_url
+from ._core import WebSocket, getdefaulttimeout
+from ._exceptions import *
+from . import _logging
 
 """
 _app.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,28 +28,43 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 """
-import selectors
-import sys
-import threading
-import time
-import traceback
-from ._abnf import ABNF
-from ._core import WebSocket, getdefaulttimeout
-from ._exceptions import *
-from . import _logging
-
 
 __all__ = ["WebSocketApp"]
 
+RECONNECT = 0
+
 
-class Dispatcher:
+def setReconnect(reconnectInterval):
+    global RECONNECT
+    RECONNECT = reconnectInterval
+
+
+class DispatcherBase:
     """
-    Dispatcher
+    DispatcherBase
     """
     def __init__(self, app, ping_timeout):
         self.app = app
         self.ping_timeout = ping_timeout
 
+    def timeout(self, seconds, callback):
+        time.sleep(seconds)
+        callback()
+
+    def reconnect(self, seconds, reconnector):
+        try:
+            while True:
+                _logging.info("reconnect() - retrying in %s seconds [%s frames in stack]" % (seconds, len(inspect.stack())))
+                time.sleep(seconds)
+                reconnector(reconnecting=True)
+        except KeyboardInterrupt as e:
+            _logging.info("User exited %s" % (e,))
+
+
+class Dispatcher(DispatcherBase):
+    """
+    Dispatcher
+    """
     def read(self, sock, read_callback, check_callback):
         while self.app.keep_running:
             sel = selectors.DefaultSelector()
@@ -55,14 +78,10 @@ class Dispatcher:
             sel.close()
 
 
-class SSLDispatcher:
+class SSLDispatcher(DispatcherBase):
     """
     SSLDispatcher
     """
-    def __init__(self, app, ping_timeout):
-        self.app = app
-        self.ping_timeout = ping_timeout
-
     def read(self, sock, read_callback, check_callback):
         while self.app.keep_running:
             r = self.select()
@@ -86,6 +105,27 @@ class SSLDispatcher:
             return r[0][0]
 
 
+class WrappedDispatcher:
+    """
+    WrappedDispatcher
+    """
+    def __init__(self, app, ping_timeout, dispatcher):
+        self.app = app
+        self.ping_timeout = ping_timeout
+        self.dispatcher = dispatcher
+        dispatcher.signal(2, dispatcher.abort)  # keyboard interrupt
+
+    def read(self, sock, read_callback, check_callback):
+        self.dispatcher.read(sock, read_callback)
+        self.ping_timeout and self.timeout(self.ping_timeout, check_callback)
+
+    def timeout(self, seconds, callback):
+        self.dispatcher.timeout(seconds, callback)
+
+    def reconnect(self, seconds, reconnector):
+        self.timeout(seconds, reconnector)
+
+
 class WebSocketApp:
     """
     Higher level of APIs are provided. The interface is like JavaScript WebSocket object.
@@ -97,7 +137,8 @@ class WebSocketApp:
                  on_cont_message=None,
                  keep_running=True, get_mask_key=None, cookie=None,
                  subprotocols=None,
-                 on_data=None):
+                 on_data=None,
+                 socket=None):
         """
         WebSocketApp initialization
 
@@ -153,6 +194,8 @@ class WebSocketApp:
             Cookie value.
         subprotocols: list
             List of available sub protocols. Default is None.
+        socket: socket
+            Pre-initialized stream socket.
         """
         self.url = url
         self.header = header if header is not None else []
@@ -172,6 +215,8 @@ class WebSocketApp:
         self.last_ping_tm = 0
         self.last_pong_tm = 0
         self.subprotocols = subprotocols
+        self.prepared_socket = socket
+        self.has_errored = False
 
     def send(self, data, opcode=ABNF.OPCODE_TEXT):
         """
@@ -214,9 +259,10 @@ class WebSocketApp:
                     ping_payload="",
                     http_proxy_host=None, http_proxy_port=None,
                     http_no_proxy=None, http_proxy_auth=None,
+                    http_proxy_timeout=None,
                     skip_utf8_validation=False,
                     host=None, origin=None, dispatcher=None,
-                    suppress_origin=False, proxy_type=None):
+                    suppress_origin=False, proxy_type=None, reconnect=None):
         """
         Run event loop for WebSocket framework.
 
@@ -244,6 +290,10 @@ class WebSocketApp:
             HTTP proxy port. If not set, set to 80.
         http_no_proxy: list
             Whitelisted host names that don't use the proxy.
+        http_proxy_timeout: int or float
+            HTTP proxy timeout, default is 60 sec as per python-socks.
+        http_proxy_auth: tuple
+            HTTP proxy auth information. tuple of username and password. Default is None.
         skip_utf8_validation: bool
             skip utf8 validation.
         host: str
@@ -254,13 +304,21 @@ class WebSocketApp:
             customize reading data from socket.
         suppress_origin: bool
             suppress outputting origin header.
+        proxy_type: str
+            type of proxy from: http, socks4, socks4a, socks5, socks5h
+        reconnect: int
+            delay interval when reconnecting
 
         Returns
         -------
         teardown: bool
-            False if caught KeyboardInterrupt, True if other exception was raised during a loop
+            False if the `WebSocketApp` is closed or caught KeyboardInterrupt,
+            True if any other exception was raised during a loop.
         """
 
+        if reconnect is None:
+            reconnect = RECONNECT
+
         if ping_timeout is not None and ping_timeout <= 0:
             raise WebSocketException("Ensure ping_timeout > 0")
         if ping_interval is not None and ping_interval < 0:
@@ -302,82 +360,105 @@ class WebSocketApp:
             # Finally call the callback AFTER all teardown is complete
             self._callback(self.on_close, close_status_code, close_reason)
 
-        try:
+        def setSock(reconnecting=False):
             self.sock = WebSocket(
                 self.get_mask_key, sockopt=sockopt, sslopt=sslopt,
                 fire_cont_frame=self.on_cont_message is not None,
                 skip_utf8_validation=skip_utf8_validation,
                 enable_multithread=True)
             self.sock.settimeout(getdefaulttimeout())
-            self.sock.connect(
-                self.url, header=self.header, cookie=self.cookie,
-                http_proxy_host=http_proxy_host,
-                http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy,
-                http_proxy_auth=http_proxy_auth, subprotocols=self.subprotocols,
-                host=host, origin=origin, suppress_origin=suppress_origin,
-                proxy_type=proxy_type)
-            if not dispatcher:
-                dispatcher = self.create_dispatcher(ping_timeout)
-
-            self._callback(self.on_open)
-
-            if ping_interval:
-                event = threading.Event()
-                thread = threading.Thread(
-                    target=self._send_ping, args=(ping_interval, event, ping_payload))
-                thread.daemon = True
-                thread.start()
-
-            def read():
-                if not self.keep_running:
-                    return teardown()
+            try:
+                self.sock.connect(
+                    self.url, header=self.header, cookie=self.cookie,
+                    http_proxy_host=http_proxy_host,
+                    http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy,
+                    http_proxy_auth=http_proxy_auth, http_proxy_timeout=http_proxy_timeout,
+                    subprotocols=self.subprotocols,
+                    host=host, origin=origin, suppress_origin=suppress_origin,
+                    proxy_type=proxy_type, socket=self.prepared_socket)
+
+                self._callback(self.on_open)
+
+                _logging.info("websocket connected")
+                dispatcher.read(self.sock.sock, read, check)
+            except (WebSocketConnectionClosedException, ConnectionRefusedError, KeyboardInterrupt, SystemExit, Exception) as e:
+                _logging.error("%s - %s" % (e, reconnect and "reconnecting" or "goodbye"))
+                reconnecting or handleDisconnect(e)
+
+        def read():
+            if not self.keep_running:
+                return teardown()
 
+            try:
                 op_code, frame = self.sock.recv_data_frame(True)
-                if op_code == ABNF.OPCODE_CLOSE:
-                    return teardown(frame)
-                elif op_code == ABNF.OPCODE_PING:
-                    self._callback(self.on_ping, frame.data)
-                elif op_code == ABNF.OPCODE_PONG:
-                    self.last_pong_tm = time.time()
-                    self._callback(self.on_pong, frame.data)
-                elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:
-                    self._callback(self.on_data, frame.data,
-                                   frame.opcode, frame.fin)
-                    self._callback(self.on_cont_message,
-                                   frame.data, frame.fin)
+            except (WebSocketConnectionClosedException, KeyboardInterrupt) as e:
+                if custom_dispatcher:
+                    return handleDisconnect(e)
                 else:
-                    data = frame.data
-                    if op_code == ABNF.OPCODE_TEXT:
-                        data = data.decode("utf-8")
-                    self._callback(self.on_data, data, frame.opcode, True)
-                    self._callback(self.on_message, data)
-
-                return True
-
-            def check():
-                if (ping_timeout):
-                    has_timeout_expired = time.time() - self.last_ping_tm > ping_timeout
-                    has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0
-                    has_pong_arrived_too_late = self.last_pong_tm - self.last_ping_tm > ping_timeout
-
-                    if (self.last_ping_tm and
-                            has_timeout_expired and
-                            (has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)):
-                        raise WebSocketTimeoutException("ping/pong timed out")
-                return True
-
-            dispatcher.read(self.sock.sock, read, check)
-        except (Exception, KeyboardInterrupt, SystemExit) as e:
+                    raise e
+            if op_code == ABNF.OPCODE_CLOSE:
+                return teardown(frame)
+            elif op_code == ABNF.OPCODE_PING:
+                self._callback(self.on_ping, frame.data)
+            elif op_code == ABNF.OPCODE_PONG:
+                self.last_pong_tm = time.time()
+                self._callback(self.on_pong, frame.data)
+            elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:
+                self._callback(self.on_data, frame.data,
+                               frame.opcode, frame.fin)
+                self._callback(self.on_cont_message,
+                               frame.data, frame.fin)
+            else:
+                data = frame.data
+                if op_code == ABNF.OPCODE_TEXT and not skip_utf8_validation:
+                    data = data.decode("utf-8")
+                self._callback(self.on_data, data, frame.opcode, True)
+                self._callback(self.on_message, data)
+
+            return True
+
+        def check():
+            if (ping_timeout):
+                has_timeout_expired = time.time() - self.last_ping_tm > ping_timeout
+                has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0
+                has_pong_arrived_too_late = self.last_pong_tm - self.last_ping_tm > ping_timeout
+
+                if (self.last_ping_tm and
+                        has_timeout_expired and
+                        (has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)):
+                    raise WebSocketTimeoutException("ping/pong timed out")
+            return True
+
+        def handleDisconnect(e):
+            self.has_errored = True
             self._callback(self.on_error, e)
             if isinstance(e, SystemExit):
                 # propagate SystemExit further
                 raise
-            teardown()
-            return not isinstance(e, KeyboardInterrupt)
-
-    def create_dispatcher(self, ping_timeout):
+            if reconnect and not isinstance(e, KeyboardInterrupt):
+                _logging.info("websocket disconnected (retrying in %s seconds) [%s frames in stack]" % (reconnect, len(inspect.stack())))
+                dispatcher.reconnect(reconnect, setSock)
+            else:
+                teardown()
+
+        custom_dispatcher = bool(dispatcher)
+        dispatcher = self.create_dispatcher(ping_timeout, dispatcher, parse_url(self.url)[3])
+
+        if ping_interval:
+            event = threading.Event()
+            thread = threading.Thread(
+                target=self._send_ping, args=(ping_interval, event, ping_payload))
+            thread.daemon = True
+            thread.start()
+
+        setSock()
+        return self.has_errored
+
+    def create_dispatcher(self, ping_timeout, dispatcher=None, is_ssl=False):
+        if dispatcher:  # If custom dispatcher is set, use WrappedDispatcher
+            return WrappedDispatcher(self, ping_timeout, dispatcher)
         timeout = ping_timeout or 10
-        if self.sock.is_ssl():
+        if is_ssl:
             return SSLDispatcher(self, timeout)
 
         return Dispatcher(self, timeout)
diff --git a/websocket/_cookiejar.py b/websocket/_cookiejar.py
index 8785383..5476d1d 100644
--- a/websocket/_cookiejar.py
+++ b/websocket/_cookiejar.py
@@ -1,12 +1,10 @@
-"""
-
-"""
+import http.cookies
 
 """
 _cookiejar.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,7 +18,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 """
-import http.cookies
 
 
 class SimpleCookieJar:
diff --git a/websocket/_core.py b/websocket/_core.py
index e26c8b1..1d68829 100644
--- a/websocket/_core.py
+++ b/websocket/_core.py
@@ -1,14 +1,23 @@
-"""
-_core.py
-====================================
-WebSocket Python client
-"""
+import socket
+import struct
+import threading
+import time
+
+# websocket modules
+from ._abnf import *
+from ._exceptions import *
+from ._handshake import *
+from ._http import *
+from ._logging import *
+from ._socket import *
+from ._ssl_compat import *
+from ._utils import *
 
 """
 _core.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -22,20 +31,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 """
-import socket
-import struct
-import threading
-import time
-
-# websocket modules
-from ._abnf import *
-from ._exceptions import *
-from ._handshake import *
-from ._http import *
-from ._logging import *
-from ._socket import *
-from ._ssl_compat import *
-from ._utils import *
 
 __all__ = ['WebSocket', 'create_connection']
 
@@ -51,8 +46,11 @@ class WebSocket:
 
     >>> import websocket
     >>> ws = websocket.WebSocket()
-    >>> ws.connect("ws://echo.websocket.org")
+    >>> ws.connect("ws://echo.websocket.events")
+    >>> ws.recv()
+    'echo.websocket.events sponsored by Lob.com'
     >>> ws.send("Hello, Server")
+    19
     >>> ws.recv()
     'Hello, Server'
     >>> ws.close()
@@ -208,7 +206,7 @@ class WebSocket:
         If you set "header" list object, you can set your own custom header.
 
         >>> ws = WebSocket()
-        >>> ws.connect("ws://echo.websocket.org/",
+        >>> ws.connect("ws://echo.websocket.events",
                 ...     header=["User-Agent: MyProgram",
                 ...             "x-custom: header"])
 
@@ -238,6 +236,8 @@ class WebSocket:
             Whitelisted host names that don't use the proxy.
         http_proxy_auth: tuple
             HTTP proxy auth information. Tuple of username and password. Default is None.
+        http_proxy_timeout: int or float
+            HTTP proxy timeout, default is 60 sec as per python-socks.
         redirect_limit: int
             Number of redirects to follow.
         subprotocols: list
@@ -250,14 +250,14 @@ class WebSocket:
                                    options.pop('socket', None))
 
         try:
-            self.handshake_response = handshake(self.sock, *addrs, **options)
+            self.handshake_response = handshake(self.sock, url, *addrs, **options)
             for attempt in range(options.pop('redirect_limit', 3)):
                 if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES:
                     url = self.handshake_response.headers['location']
                     self.sock.close()
                     self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options),
                                                options.pop('socket', None))
-                    self.handshake_response = handshake(self.sock, *addrs, **options)
+                    self.handshake_response = handshake(self.sock, url, *addrs, **options)
             self.connected = True
         except:
             if self.sock:
@@ -286,7 +286,7 @@ class WebSocket:
         """
         Send the data frame.
 
-        >>> ws = create_connection("ws://echo.websocket.org/")
+        >>> ws = create_connection("ws://echo.websocket.events")
         >>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT)
         >>> ws.send_frame(frame)
         >>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0)
@@ -546,7 +546,7 @@ def create_connection(url, timeout=None, class_=WebSocket, **options):
     You can customize using 'options'.
     If you set "header" list object, you can set your own custom header.
 
-    >>> conn = create_connection("ws://echo.websocket.org/",
+    >>> conn = create_connection("ws://echo.websocket.events",
          ...     header=["User-Agent: MyProgram",
          ...             "x-custom: header"])
 
@@ -577,6 +577,8 @@ def create_connection(url, timeout=None, class_=WebSocket, **options):
         Whitelisted host names that don't use the proxy.
     http_proxy_auth: tuple
         HTTP proxy auth information. tuple of username and password. Default is None.
+    http_proxy_timeout: int or float
+        HTTP proxy timeout, default is 60 sec as per python-socks.
     enable_multithread: bool
         Enable lock for multithread.
     redirect_limit: int
diff --git a/websocket/_exceptions.py b/websocket/_exceptions.py
index b92b1f4..811d594 100644
--- a/websocket/_exceptions.py
+++ b/websocket/_exceptions.py
@@ -1,12 +1,8 @@
-"""
-Define WebSocket exceptions
-"""
-
 """
 _exceptions.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/websocket/_handshake.py b/websocket/_handshake.py
index f9dabb5..07a4cfb 100644
--- a/websocket/_handshake.py
+++ b/websocket/_handshake.py
@@ -2,7 +2,7 @@
 _handshake.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -47,8 +47,8 @@ class handshake_response:
         CookieJar.add(headers.get("set-cookie"))
 
 
-def handshake(sock, hostname, port, resource, **options):
-    headers, key = _get_handshake_headers(resource, hostname, port, options)
+def handshake(sock, url, hostname, port, resource, **options):
+    headers, key = _get_handshake_headers(resource, url, hostname, port, options)
 
     header_str = "\r\n".join(headers)
     send(sock, header_str)
@@ -72,7 +72,7 @@ def _pack_hostname(hostname):
     return hostname
 
 
-def _get_handshake_headers(resource, host, port, options):
+def _get_handshake_headers(resource, url, host, port, options):
     headers = [
         "GET %s HTTP/1.1" % resource,
         "Upgrade: websocket"
@@ -81,30 +81,34 @@ def _get_handshake_headers(resource, host, port, options):
         hostport = _pack_hostname(host)
     else:
         hostport = "%s:%d" % (_pack_hostname(host), port)
-    if "host" in options and options["host"] is not None:
+    if options.get("host"):
         headers.append("Host: %s" % options["host"])
     else:
         headers.append("Host: %s" % hostport)
 
-    if "suppress_origin" not in options or not options["suppress_origin"]:
+    # scheme indicates whether http or https is used in Origin
+    # The same approach is used in parse_url of _url.py to set default port
+    scheme, url = url.split(":", 1)
+    if not options.get("suppress_origin"):
         if "origin" in options and options["origin"] is not None:
             headers.append("Origin: %s" % options["origin"])
+        elif scheme == "wss":
+            headers.append("Origin: https://%s" % hostport)
         else:
             headers.append("Origin: http://%s" % hostport)
 
     key = _create_sec_websocket_key()
 
     # Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified
-    if 'header' not in options or 'Sec-WebSocket-Key' not in options['header']:
-        key = _create_sec_websocket_key()
+    if not options.get('header') or 'Sec-WebSocket-Key' not in options['header']:
         headers.append("Sec-WebSocket-Key: %s" % key)
     else:
         key = options['header']['Sec-WebSocket-Key']
 
-    if 'header' not in options or 'Sec-WebSocket-Version' not in options['header']:
+    if not options.get('header') or 'Sec-WebSocket-Version' not in options['header']:
         headers.append("Sec-WebSocket-Version: %s" % VERSION)
 
-    if 'connection' not in options or options['connection'] is None:
+    if not options.get('connection'):
         headers.append('Connection: Upgrade')
     else:
         headers.append(options['connection'])
@@ -113,8 +117,8 @@ def _get_handshake_headers(resource, host, port, options):
     if subprotocols:
         headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols))
 
-    if "header" in options:
-        header = options["header"]
+    header = options.get("header")
+    if header:
         if isinstance(header, dict):
             header = [
                 ": ".join([k, v])
diff --git a/websocket/_http.py b/websocket/_http.py
index 603fa00..17d3f8a 100644
--- a/websocket/_http.py
+++ b/websocket/_http.py
@@ -2,7 +2,7 @@
 _http.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@ import sys
 
 from ._exceptions import *
 from ._logging import *
-from ._socket import*
+from ._socket import *
 from ._ssl_compat import *
 from ._url import *
 
@@ -59,7 +59,7 @@ class proxy_info:
             self.no_proxy = options.get("http_no_proxy", None)
             self.proxy_protocol = options.get("proxy_type", "http")
             # Note: If timeout not specified, default python-socks timeout is 60 seconds
-            self.proxy_timeout = options.get("timeout", None)
+            self.proxy_timeout = options.get("http_proxy_timeout", None)
             if self.proxy_protocol not in ['http', 'socks4', 'socks4a', 'socks5', 'socks5h']:
                 raise ProxyError("Only http, socks4, socks5 proxy protocols are supported")
         else:
@@ -114,22 +114,22 @@ def connect(url, options, proxy, socket):
     if proxy.proxy_host and not socket and not (proxy.proxy_protocol == "http"):
         return _start_proxied_socket(url, options, proxy)
 
-    hostname, port, resource, is_secure = parse_url(url)
+    hostname, port_from_url, resource, is_secure = parse_url(url)
 
     if socket:
-        return socket, (hostname, port, resource)
+        return socket, (hostname, port_from_url, resource)
 
     addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
-        hostname, port, is_secure, proxy)
+        hostname, port_from_url, is_secure, proxy)
     if not addrinfo_list:
         raise WebSocketException(
-            "Host not found.: " + hostname + ":" + str(port))
+            "Host not found.: " + hostname + ":" + str(port_from_url))
 
     sock = None
     try:
         sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
         if need_tunnel:
-            sock = _tunnel(sock, hostname, port, auth)
+            sock = _tunnel(sock, hostname, port_from_url, auth)
 
         if is_secure:
             if HAVE_SSL:
@@ -137,7 +137,7 @@ def connect(url, options, proxy, socket):
             else:
                 raise WebSocketException("SSL not available.")
 
-        return sock, (hostname, port, resource)
+        return sock, (hostname, port_from_url, resource)
     except:
         if sock:
             sock.close()
@@ -184,19 +184,16 @@ def _open_socket(addrinfo_list, sockopt, timeout):
             try:
                 sock.connect(address)
             except socket.error as error:
+                sock.close()
                 error.remote_ip = str(address[0])
                 try:
-                    eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED)
-                except:
-                    eConnRefused = (errno.ECONNREFUSED, )
-                if error.errno == errno.EINTR:
-                    continue
-                elif error.errno in eConnRefused:
+                    eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED, errno.ENETUNREACH)
+                except AttributeError:
+                    eConnRefused = (errno.ECONNREFUSED, errno.ENETUNREACH)
+                if error.errno in eConnRefused:
                     err = error
                     continue
                 else:
-                    if sock:
-                        sock.close()
                     raise error
             else:
                 break
diff --git a/websocket/_logging.py b/websocket/_logging.py
index 480d43b..3921111 100644
--- a/websocket/_logging.py
+++ b/websocket/_logging.py
@@ -1,12 +1,10 @@
-"""
-
-"""
+import logging
 
 """
 _logging.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,7 +18,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 """
-import logging
 
 _logger = logging.getLogger('websocket')
 try:
@@ -38,7 +35,7 @@ __all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace",
            "isEnabledForError", "isEnabledForDebug", "isEnabledForTrace"]
 
 
-def enableTrace(traceable, handler=logging.StreamHandler()):
+def enableTrace(traceable, handler=logging.StreamHandler(), level="DEBUG"):
     """
     Turn on/off the traceability.
 
@@ -51,7 +48,7 @@ def enableTrace(traceable, handler=logging.StreamHandler()):
     _traceEnabled = traceable
     if traceable:
         _logger.addHandler(handler)
-        _logger.setLevel(logging.DEBUG)
+        _logger.setLevel(getattr(logging, level))
 
 
 def dump(title, message):
@@ -73,6 +70,10 @@ def debug(msg):
     _logger.debug(msg)
 
 
+def info(msg):
+    _logger.info(msg)
+
+
 def trace(msg):
     if _traceEnabled:
         _logger.debug(msg)
diff --git a/websocket/_socket.py b/websocket/_socket.py
index 4d9cc09..54e6399 100644
--- a/websocket/_socket.py
+++ b/websocket/_socket.py
@@ -1,12 +1,16 @@
-"""
+import errno
+import selectors
+import socket
 
-"""
+from ._exceptions import *
+from ._ssl_compat import *
+from ._utils import *
 
 """
 _socket.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,13 +24,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 """
-import errno
-import selectors
-import socket
-
-from ._exceptions import *
-from ._ssl_compat import *
-from ._utils import *
 
 DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)]
 if hasattr(socket, "SO_KEEPALIVE"):
@@ -92,9 +89,7 @@ def recv(sock, bufsize):
             pass
         except socket.error as exc:
             error_code = extract_error_code(exc)
-            if error_code is None:
-                raise
-            if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK:
+            if error_code != errno.EAGAIN and error_code != errno.EWOULDBLOCK:
                 raise
 
         sel = selectors.DefaultSelector()
@@ -111,6 +106,8 @@ def recv(sock, bufsize):
             bytes_ = sock.recv(bufsize)
         else:
             bytes_ = _recv()
+    except TimeoutError:
+        raise WebSocketTimeoutException("Connection timed out")
     except socket.timeout as e:
         message = extract_err_message(e)
         raise WebSocketTimeoutException(message)
diff --git a/websocket/_ssl_compat.py b/websocket/_ssl_compat.py
index f4af524..e227840 100644
--- a/websocket/_ssl_compat.py
+++ b/websocket/_ssl_compat.py
@@ -2,7 +2,7 @@
 _ssl_compat.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/websocket/_url.py b/websocket/_url.py
index f2a5501..2d3d265 100644
--- a/websocket/_url.py
+++ b/websocket/_url.py
@@ -1,11 +1,14 @@
-"""
+import os
+import socket
+import struct
+
+from urllib.parse import unquote, urlparse
 
-"""
 """
 _url.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,13 +23,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 """
 
-import os
-import socket
-import struct
-
-from urllib.parse import unquote, urlparse
-
-
 __all__ = ["parse_url", "get_proxy_info"]
 
 
diff --git a/websocket/_utils.py b/websocket/_utils.py
index 21fc437..fdcf345 100644
--- a/websocket/_utils.py
+++ b/websocket/_utils.py
@@ -2,7 +2,7 @@
 _url.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/websocket/_wsdump.py b/websocket/_wsdump.py
index 4d15f41..860ac34 100755
--- a/websocket/_wsdump.py
+++ b/websocket/_wsdump.py
@@ -4,7 +4,7 @@
 wsdump.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -64,7 +64,7 @@ class VAction(argparse.Action):
 def parse_args():
     parser = argparse.ArgumentParser(description="WebSocket Simple Dump Tool")
     parser.add_argument("url", metavar="ws_url",
-                        help="websocket url. ex. ws://echo.websocket.org/")
+                        help="websocket url. ex. ws://echo.websocket.events/")
     parser.add_argument("-p", "--proxy",
                         help="proxy url. ex. http://127.0.0.1:8080")
     parser.add_argument("-v", "--verbose", default=0, nargs='?', action=VAction,
diff --git a/websocket/tests/data/header03.txt b/websocket/tests/data/header03.txt
index 030e13a..1a81dc7 100644
--- a/websocket/tests/data/header03.txt
+++ b/websocket/tests/data/header03.txt
@@ -3,5 +3,6 @@ Connection: Upgrade, Keep-Alive
 Upgrade: WebSocket
 Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
 Set-Cookie: Token=ABCDE
+Set-Cookie: Token=FGHIJ
 some_header: something
 
diff --git a/websocket/tests/test_abnf.py b/websocket/tests/test_abnf.py
index 7f156dc..7c9d89d 100644
--- a/websocket/tests/test_abnf.py
+++ b/websocket/tests/test_abnf.py
@@ -1,10 +1,14 @@
 # -*- coding: utf-8 -*-
 #
+import websocket as ws
+from websocket._abnf import *
+import unittest
+
 """
 test_abnf.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,10 +23,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 """
 
-import websocket as ws
-from websocket._abnf import *
-import unittest
-
 
 class ABNFTest(unittest.TestCase):
 
diff --git a/websocket/tests/test_app.py b/websocket/tests/test_app.py
index cd1146b..5526d3e 100644
--- a/websocket/tests/test_app.py
+++ b/websocket/tests/test_app.py
@@ -1,10 +1,17 @@
 # -*- coding: utf-8 -*-
 #
+import os
+import os.path
+import threading
+import websocket as ws
+import ssl
+import unittest
+
 """
 test_app.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,12 +26,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 """
 
-import os
-import os.path
-import websocket as ws
-import ssl
-import unittest
-
 # Skip test to access the internet unless TEST_WITH_INTERNET == 1
 TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1'
 # Skip tests relying on local websockets server unless LOCAL_WS_SERVER_PORT != -1
@@ -45,11 +46,13 @@ class WebSocketAppTest(unittest.TestCase):
         WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()
         WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()
         WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()
+        WebSocketAppTest.on_error_data = WebSocketAppTest.NotSetYet()
 
     def tearDown(self):
         WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()
         WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()
         WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()
+        WebSocketAppTest.on_error_data = WebSocketAppTest.NotSetYet()
 
     @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled")
     def testKeepRunning(self):
@@ -77,6 +80,57 @@ class WebSocketAppTest(unittest.TestCase):
         app = ws.WebSocketApp('ws://127.0.0.1:' + LOCAL_WS_SERVER_PORT, on_open=on_open, on_close=on_close, on_message=on_message)
         app.run_forever()
 
+#    @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled")
+    @unittest.skipUnless(False, "Test disabled for now (requires rel)")
+    def testRunForeverDispatcher(self):
+        """ A WebSocketApp should keep running as long as its self.keep_running
+        is not False (in the boolean context).
+        """
+
+        def on_open(self, *args, **kwargs):
+            """ Send a message, receive, and send one more
+            """
+            self.send("hello!")
+            self.recv()
+            self.send("goodbye!")
+
+        def on_message(wsapp, message):
+            print(message)
+            self.close()
+
+        app = ws.WebSocketApp('ws://127.0.0.1:' + LOCAL_WS_SERVER_PORT, on_open=on_open, on_message=on_message)
+        app.run_forever(dispatcher="Dispatcher")  # doesn't work
+#        app.run_forever(dispatcher=rel)          # would work
+#        rel.dispatch()
+
+    @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled")
+    def testRunForeverTeardownCleanExit(self):
+        """ The WebSocketApp.run_forever() method should return `False` when the application ends gracefully.
+        """
+        app = ws.WebSocketApp('ws://127.0.0.1:' + LOCAL_WS_SERVER_PORT)
+        threading.Timer(interval=0.2, function=app.close).start()
+        teardown = app.run_forever()
+        self.assertEqual(teardown, False)
+
+    @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled")
+    def testRunForeverTeardownExceptionalExit(self):
+        """ The WebSocketApp.run_forever() method should return `True` when the application ends with an exception.
+        It should also invoke the `on_error` callback before exiting.
+        """
+
+        def break_it():
+            # Deliberately break the WebSocketApp by closing the inner socket.
+            app.sock.close()
+
+        def on_error(_, err):
+            WebSocketAppTest.on_error_data = str(err)
+
+        app = ws.WebSocketApp('ws://127.0.0.1:' + LOCAL_WS_SERVER_PORT, on_error=on_error)
+        threading.Timer(interval=0.2, function=break_it).start()
+        teardown = app.run_forever(ping_timeout=0.1)
+        self.assertEqual(teardown, True)
+        self.assertTrue(len(WebSocketAppTest.on_error_data) > 0)
+
     @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
     def testSockMaskKey(self):
         """ A WebSocketApp should forward the received mask_key function down
@@ -86,7 +140,7 @@ class WebSocketAppTest(unittest.TestCase):
         def my_mask_key_func():
             return "\x00\x00\x00\x00"
 
-        app = ws.WebSocketApp('wss://stream.meetup.com/2/rsvps', get_mask_key=my_mask_key_func)
+        app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1', get_mask_key=my_mask_key_func)
 
         # if numpy is installed, this assertion fail
         # Note: We can't use 'is' for comparing the functions directly, need to use 'id'.
@@ -136,8 +190,8 @@ class WebSocketAppTest(unittest.TestCase):
     def testOpcodeBinary(self):
         """ Test WebSocketApp binary opcode
         """
-
-        app = ws.WebSocketApp('streaming.vn.teslamotors.com/streaming/')
+        # The lack of wss:// in the URL below is on purpose
+        app = ws.WebSocketApp('wss://streaming.vn.teslamotors.com/streaming/')
         app.run_forever(ping_interval=2, ping_timeout=1, ping_payload="Ping payload")
 
     @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
diff --git a/websocket/tests/test_cookiejar.py b/websocket/tests/test_cookiejar.py
index 5bf1fca..559b2e0 100644
--- a/websocket/tests/test_cookiejar.py
+++ b/websocket/tests/test_cookiejar.py
@@ -1,12 +1,11 @@
-"""
-
-"""
+import unittest
+from websocket._cookiejar import SimpleCookieJar
 
 """
 test_cookiejar.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,8 +19,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 """
-import unittest
-from websocket._cookiejar import SimpleCookieJar
 
 
 class CookieJarTest(unittest.TestCase):
diff --git a/websocket/tests/test_http.py b/websocket/tests/test_http.py
index fda467d..ffcbde2 100644
--- a/websocket/tests/test_http.py
+++ b/websocket/tests/test_http.py
@@ -1,10 +1,19 @@
 # -*- coding: utf-8 -*-
 #
+import os
+import os.path
+import websocket as ws
+from websocket._http import proxy_info, read_headers, _start_proxied_socket, _tunnel, _get_addrinfo_list, connect
+import unittest
+import ssl
+import websocket
+import socket
+
 """
 test_http.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,15 +28,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 """
 
-import os
-import os.path
-import websocket as ws
-from websocket._http import proxy_info, read_headers, _start_proxied_socket, _tunnel, _get_addrinfo_list, connect
-import unittest
-import ssl
-import websocket
-import socket
-
 try:
     from python_socks._errors import ProxyError, ProxyTimeoutError, ProxyConnectionError
 except:
@@ -105,15 +105,15 @@ class HttpTest(unittest.TestCase):
         if ws._http.HAVE_PYTHON_SOCKS:
             # Need this check, otherwise case where python_socks is not installed triggers
             # websocket._exceptions.WebSocketException: Python Socks is needed for SOCKS proxying but is not available
-            self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks4", timeout=1))
-            self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks4a", timeout=1))
-            self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks5", timeout=1))
-            self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks5h", timeout=1))
-            self.assertRaises(ProxyConnectionError, connect, "wss://example.com", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port=9999, proxy_type="socks4", timeout=1), None)
+            self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks4", http_proxy_timeout=1))
+            self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks4a", http_proxy_timeout=1))
+            self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks5", http_proxy_timeout=1))
+            self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks5h", http_proxy_timeout=1))
+            self.assertRaises(ProxyConnectionError, connect, "wss://example.com", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port=9999, proxy_type="socks4", http_proxy_timeout=1), None)
 
         self.assertRaises(TypeError, _get_addrinfo_list, None, 80, True, proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="9999", proxy_type="http"))
         self.assertRaises(TypeError, _get_addrinfo_list, None, 80, True, proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="9999", proxy_type="http"))
-        self.assertRaises(socket.timeout, connect, "wss://google.com", OptsList(), proxy_info(http_proxy_host="8.8.8.8", http_proxy_port=9999, proxy_type="http", timeout=1), None)
+        self.assertRaises(socket.timeout, connect, "wss://google.com", OptsList(), proxy_info(http_proxy_host="8.8.8.8", http_proxy_port=9999, proxy_type="http", http_proxy_timeout=1), None)
         self.assertEqual(
             connect("wss://google.com", OptsList(), proxy_info(http_proxy_host="8.8.8.8", http_proxy_port=8080, proxy_type="http"), True),
             (True, ("google.com", 443, "/")))
diff --git a/websocket/tests/test_url.py b/websocket/tests/test_url.py
index ad3a3b1..7e155fd 100644
--- a/websocket/tests/test_url.py
+++ b/websocket/tests/test_url.py
@@ -1,10 +1,14 @@
 # -*- coding: utf-8 -*-
 #
+import os
+import unittest
+from websocket._url import get_proxy_info, parse_url, _is_address_in_network, _is_no_proxy_host
+
 """
 test_url.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,10 +23,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 """
 
-import os
-import unittest
-from websocket._url import get_proxy_info, parse_url, _is_address_in_network, _is_no_proxy_host
-
 
 class UrlTest(unittest.TestCase):
 
@@ -209,73 +209,73 @@ class ProxyInfoTest(unittest.TestCase):
             del os.environ["no_proxy"]
 
     def testProxyFromArgs(self):
-        self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost"), ("localhost", 0, None))
-        self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128),
+        self.assertEqual(get_proxy_info("echo.websocket.events", False, proxy_host="localhost"), ("localhost", 0, None))
+        self.assertEqual(get_proxy_info("echo.websocket.events", False, proxy_host="localhost", proxy_port=3128),
                          ("localhost", 3128, None))
-        self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost"), ("localhost", 0, None))
-        self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128),
+        self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost"), ("localhost", 0, None))
+        self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_port=3128),
                          ("localhost", 3128, None))
 
-        self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_auth=("a", "b")),
+        self.assertEqual(get_proxy_info("echo.websocket.events", False, proxy_host="localhost", proxy_auth=("a", "b")),
                          ("localhost", 0, ("a", "b")))
         self.assertEqual(
-            get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
+            get_proxy_info("echo.websocket.events", False, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
             ("localhost", 3128, ("a", "b")))
-        self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_auth=("a", "b")),
+        self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_auth=("a", "b")),
                          ("localhost", 0, ("a", "b")))
         self.assertEqual(
-            get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
+            get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
             ("localhost", 3128, ("a", "b")))
 
-        self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128,
+        self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_port=3128,
                                         no_proxy=["example.com"], proxy_auth=("a", "b")),
                          ("localhost", 3128, ("a", "b")))
-        self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128,
-                                        no_proxy=["echo.websocket.org"], proxy_auth=("a", "b")),
+        self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_port=3128,
+                                        no_proxy=["echo.websocket.events"], proxy_auth=("a", "b")),
                          (None, 0, None))
 
     def testProxyFromEnv(self):
         os.environ["http_proxy"] = "http://localhost/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None))
+        self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, None))
         os.environ["http_proxy"] = "http://localhost:3128/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None))
+        self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", 3128, None))
 
         os.environ["http_proxy"] = "http://localhost/"
         os.environ["https_proxy"] = "http://localhost2/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None))
+        self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, None))
         os.environ["http_proxy"] = "http://localhost:3128/"
         os.environ["https_proxy"] = "http://localhost2:3128/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None))
+        self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", 3128, None))
 
         os.environ["http_proxy"] = "http://localhost/"
         os.environ["https_proxy"] = "http://localhost2/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, None))
+        self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", None, None))
         os.environ["http_proxy"] = "http://localhost:3128/"
         os.environ["https_proxy"] = "http://localhost2:3128/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, None))
+        self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", 3128, None))
 
         os.environ["http_proxy"] = "http://a:b@localhost/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b")))
+        self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, ("a", "b")))
         os.environ["http_proxy"] = "http://a:b@localhost:3128/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b")))
+        self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", 3128, ("a", "b")))
 
         os.environ["http_proxy"] = "http://a:b@localhost/"
         os.environ["https_proxy"] = "http://a:b@localhost2/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b")))
+        self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, ("a", "b")))
         os.environ["http_proxy"] = "http://a:b@localhost:3128/"
         os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b")))
+        self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", 3128, ("a", "b")))
 
         os.environ["http_proxy"] = "http://a:b@localhost/"
         os.environ["https_proxy"] = "http://a:b@localhost2/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, ("a", "b")))
+        self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", None, ("a", "b")))
         os.environ["http_proxy"] = "http://a:b@localhost:3128/"
         os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, ("a", "b")))
+        self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", 3128, ("a", "b")))
 
         os.environ["http_proxy"] = "http://john%40example.com:P%40SSWORD@localhost:3128/"
         os.environ["https_proxy"] = "http://john%40example.com:P%40SSWORD@localhost2:3128/"
-        self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, ("john@example.com", "P@SSWORD")))
+        self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", 3128, ("john@example.com", "P@SSWORD")))
 
         os.environ["http_proxy"] = "http://a:b@localhost/"
         os.environ["https_proxy"] = "http://a:b@localhost2/"
@@ -283,12 +283,12 @@ class ProxyInfoTest(unittest.TestCase):
         self.assertEqual(get_proxy_info("example.1.com", True), ("localhost2", None, ("a", "b")))
         os.environ["http_proxy"] = "http://a:b@localhost:3128/"
         os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
-        os.environ["no_proxy"] = "example1.com,example2.com, echo.websocket.org"
-        self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None))
+        os.environ["no_proxy"] = "example1.com,example2.com, echo.websocket.events"
+        self.assertEqual(get_proxy_info("echo.websocket.events", True), (None, 0, None))
         os.environ["http_proxy"] = "http://a:b@localhost:3128/"
         os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
-        os.environ["no_proxy"] = "example1.com,example2.com, .websocket.org"
-        self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None))
+        os.environ["no_proxy"] = "example1.com,example2.com, .websocket.events"
+        self.assertEqual(get_proxy_info("echo.websocket.events", True), (None, 0, None))
 
         os.environ["http_proxy"] = "http://a:b@localhost:3128/"
         os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
diff --git a/websocket/tests/test_websocket.py b/websocket/tests/test_websocket.py
index 8b34aa5..d47d73e 100644
--- a/websocket/tests/test_websocket.py
+++ b/websocket/tests/test_websocket.py
@@ -1,14 +1,21 @@
 # -*- coding: utf-8 -*-
 #
-"""
-
-"""
+import os
+import os.path
+import socket
+import websocket as ws
+import unittest
+from websocket._handshake import _create_sec_websocket_key, \
+    _validate as _validate_header
+from websocket._http import read_headers
+from websocket._utils import validate_utf8
+from base64 import decodebytes as base64decode
 
 """
 test_websocket.py
 websocket - WebSocket client library for Python
 
-Copyright 2021 engn33r
+Copyright 2022 engn33r
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -23,18 +30,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 """
 
-import os
-import os.path
-import socket
-import websocket as ws
-from websocket._handshake import _create_sec_websocket_key, \
-    _validate as _validate_header
-from websocket._http import read_headers
-from websocket._utils import validate_utf8
-from base64 import decodebytes as base64decode
-
-import unittest
-
 try:
     import ssl
     from ssl import SSLError
@@ -201,14 +196,16 @@ class WebSocketTest(unittest.TestCase):
     @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
     def testIter(self):
         count = 2
-        for _ in ws.create_connection('wss://stream.meetup.com/2/rsvps'):
+        s = ws.create_connection('wss://api.bitfinex.com/ws/2')
+        s.send('{"event": "subscribe", "channel": "ticker"}')
+        for _ in s:
             count -= 1
             if count == 0:
                 break
 
     @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
     def testNext(self):
-        sock = ws.create_connection('wss://stream.meetup.com/2/rsvps')
+        sock = ws.create_connection('wss://api.bitfinex.com/ws/2')
         self.assertEqual(str, type(next(sock)))
 
     def testInternalRecvStrict(self):
@@ -383,6 +380,7 @@ class WebSocketTest(unittest.TestCase):
         s = ws.create_connection("ws://127.0.0.1:" + LOCAL_WS_SERVER_PORT,
                                  headers={"User-Agent": "PythonWebsocketClient"})
         self.assertNotEqual(s, None)
+        self.assertEqual(s.getsubprotocol(), None)
         s.send("Hello, World")
         result = s.recv()
         self.assertEqual(result, "Hello, World")
@@ -434,7 +432,7 @@ class HandshakeTest(unittest.TestCase):
         self.assertRaises(ws._exceptions.WebSocketBadStatusException,
                           websock3.connect, "wss://api.bitfinex.com/ws/2", cookie="chocolate",
                           origin="testing_websockets.com",
-                          host="echo.websocket.org/websocket-client-test",
+                          host="echo.websocket.events/websocket-client-test",
                           subprotocols=["testproto"],
                           connection="Upgrade",
                           header={"CustomHeader1":"123",
diff --git a/websocket_client.egg-info/PKG-INFO b/websocket_client.egg-info/PKG-INFO
index 116110c..b0b7e1d 100644
--- a/websocket_client.egg-info/PKG-INFO
+++ b/websocket_client.egg-info/PKG-INFO
@@ -1,35 +1,34 @@
 Metadata-Version: 2.1
 Name: websocket-client
-Version: 1.2.3
+Version: 1.4.2
 Summary: WebSocket client for Python with low level API options
 Home-page: https://github.com/websocket-client/websocket-client.git
+Download-URL: https://github.com/websocket-client/websocket-client/releases
 Author: liris
 Author-email: liris.pp@gmail.com
 License: Apache-2.0
-Download-URL: https://github.com/websocket-client/websocket-client/releases
 Project-URL: Documentation, https://websocket-client.readthedocs.io/
 Project-URL: Source, https://github.com/websocket-client/websocket-client/
 Keywords: websockets client
-Platform: UNKNOWN
 Classifier: Development Status :: 4 - Beta
 Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
 Classifier: Operating System :: MacOS :: MacOS X
 Classifier: Operating System :: POSIX
 Classifier: Operating System :: Microsoft :: Windows
 Classifier: Topic :: Internet
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Classifier: Intended Audience :: Developers
-Requires-Python: >=3.6
+Requires-Python: >=3.7
 Description-Content-Type: text/markdown
-Provides-Extra: test
-Provides-Extra: optional
 Provides-Extra: docs
+Provides-Extra: optional
+Provides-Extra: test
 License-File: LICENSE
 
 [![docs](https://readthedocs.org/projects/websocket-client/badge/?style=flat)](https://websocket-client.readthedocs.io/)
@@ -59,7 +58,7 @@ Please see the [contribution guidelines](https://github.com/websocket-client/web
 ## Installation
 
 You can use either `python3 setup.py install` or `pip3 install websocket-client`
-to install. This module is tested on Python 3.6+.
+to install. This module is tested on Python 3.7+.
 
 There are several optional dependencies that can be installed to enable
 specific websocket-client features.
@@ -70,6 +69,9 @@ specific websocket-client features.
 - To install `Sphinx` and `sphinx_rtd_theme` to build project documentation, use:
  `pip3 install websocket-client[docs]`
 
+While not a strict dependency, [rel](https://github.com/bubbleboy14/registeredeventlistener)
+is useful when using `run_forever` with automatic reconnect. Install rel with `pip3 install rel`.
+
 Footnote: Some shells, such as zsh, require you to escape the `[` and `]` characters with a `\`.
 
 ## Usage Tips
@@ -102,13 +104,28 @@ Many more examples are found in the
 ### Long-lived Connection
 
 Most real-world WebSockets situations involve longer-lived connections.
-The WebSocketApp `run_forever` loop automatically tries to reconnect when a
-connection is lost, and provides a variety of event-based connection controls.
+The WebSocketApp `run_forever` loop will automatically try to reconnect
+to an open WebSocket connection when a network
+connection is lost if it is provided with:
+
+- a `dispatcher` argument (async dispatcher like rel or pyevent)
+- a non-zero `reconnect` argument (delay between disconnection and attempted reconnection)
+
+`run_forever` provides a variety of event-based connection controls
+using callbacks like `on_message` and `on_error`.
+`run_forever` does not automatically reconnect if the server
+closes the WebSocket gracefully (returning
+[a standard websocket close code](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1)).
+Customizing behavior when the server closes
+the WebSocket should be handled in the `on_close` callback.
+This example uses [rel](https://github.com/bubbleboy14/registeredeventlistener)
+for the dispatcher to provide automatic reconnection.
 
 ```python
 import websocket
 import _thread
 import time
+import rel
 
 def on_message(ws, message):
     print(message)
@@ -120,24 +137,19 @@ def on_close(ws, close_status_code, close_msg):
     print("### closed ###")
 
 def on_open(ws):
-    def run(*args):
-        for i in range(3):
-            time.sleep(1)
-            ws.send("Hello %d" % i)
-        time.sleep(1)
-        ws.close()
-        print("thread terminating...")
-    _thread.start_new_thread(run, ())
+    print("Opened connection")
 
 if __name__ == "__main__":
     websocket.enableTrace(True)
-    ws = websocket.WebSocketApp("ws://echo.websocket.org/",
+    ws = websocket.WebSocketApp("wss://api.gemini.com/v1/marketdata/BTCUSD",
                               on_open=on_open,
                               on_message=on_message,
                               on_error=on_error,
                               on_close=on_close)
 
-    ws.run_forever()
+    ws.run_forever(dispatcher=rel, reconnect=5)  # Set dispatcher to automatic reconnection, 5 second reconnect delay if connection closed unexpectedly
+    rel.signal(2, rel.abort)  # Keyboard Interrupt
+    rel.dispatch()
 ```
 
 ### Short-lived Connection
@@ -148,7 +160,9 @@ server is running and responds properly to a specific request.
 
 ```python
 from websocket import create_connection
-ws = create_connection("ws://echo.websocket.org/")
+
+ws = create_connection("ws://echo.websocket.events/")
+print(ws.recv())
 print("Sending 'Hello, World'...")
 ws.send("Hello, World")
 print("Sent")
@@ -157,18 +171,3 @@ result =  ws.recv()
 print("Received '%s'" % result)
 ws.close()
 ```
-
-If you want to customize socket options, set sockopt, as seen below:
-
-```python
-from websocket import create_connection
-ws = create_connection("ws://echo.websocket.org/",
-                        sockopt=((socket.IPPROTO_TCP, socket.TCP_NODELAY),))
-```
-
-### Acknowledgements
-
-Thanks to @battlemidget and @ralphbean for helping migrate this project to
-Python 3.
-
-
diff --git a/websocket_client.egg-info/SOURCES.txt b/websocket_client.egg-info/SOURCES.txt
index b62ab51..d613f2b 100644
--- a/websocket_client.egg-info/SOURCES.txt
+++ b/websocket_client.egg-info/SOURCES.txt
@@ -6,6 +6,7 @@ setup.cfg
 setup.py
 examples/echo_client.py
 examples/echoapp_client.py
+examples/rel_client.py
 websocket/__init__.py
 websocket/_abnf.py
 websocket/_app.py
diff --git a/websocket_client.egg-info/entry_points.txt b/websocket_client.egg-info/entry_points.txt
index 2c30a29..45c854e 100644
--- a/websocket_client.egg-info/entry_points.txt
+++ b/websocket_client.egg-info/entry_points.txt
@@ -1,3 +1,2 @@
 [console_scripts]
 wsdump = websocket._wsdump:main
-

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.4.2.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.4.2.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.4.2.egg-info/entry_points.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.4.2.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.4.2.egg-info/top_level.txt
-rw-r--r--  root/root   /usr/share/doc/python3-websocket/examples/rel_client.py

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.2.3.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.2.3.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.2.3.egg-info/entry_points.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.2.3.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/websocket_client-1.2.3.egg-info/top_level.txt

No differences were encountered in the control files

More details

Full run details