Repository: mitmproxy/mitmproxy

Issue:
tornado.iostream.StreamClosedError
#### Problem Description
I honestly have no idea. The terminal popped an error message saying:
```python
Traceback (most recent call last):

  File "/opt/homebrew/Cellar/mitmproxy/8.1.0/libexec/lib/python3.10/site-packages/tornado/websocket.py", line 1102, in wrapper
    await fut

tornado.iostream.StreamClosedError: Stream is closed


During handling of the above exception, another exception occurred:


Traceback (most recent call last):

  File "/opt/homebrew/Cellar/mitmproxy/8.1.0/libexec/lib/python3.10/site-packages/tornado/websocket.py", line 1104, in wrapper
    raise WebSocketClosedError()

tornado.websocket.WebSocketClosedError

Please lodge a bug report at:
	https://github.com/mitmproxy/mitmproxy/issues
```
and I thought I'll be a good citizen and report the error.
#### Steps to reproduce the behavior:
1. 
2. 
3. 

#### System Information
Mitmproxy: 8.1.0
Python:    3.10.4
OpenSSL:   OpenSSL 1.1.1o  3 May 2022
Platform:  macOS-12.4-arm64-arm-64bit
<img width="1624" alt="Screenshot 2022-06-08 at 15 33 53" src="https://user-images.githubusercontent.com/75550932/172617337-6dffe0c3-0d9b-430e-a92b-31b6897f1b7c.png">



Discussion:
I was able to reproduce this once but not consistently. I had two browsers open at `8081`, then opening a browser that proxied a website with lot of requests and then closed one of the mitmweb clients. It actually looked like this:

```
// hundreds more of these
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
tornado.websocket.WebSocketClosedError
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
tornado.websocket.WebSocketClosedError
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
tornado.websocket.WebSocketClosedError
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
tornado.websocket.WebSocketClosedError
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
tornado.websocket.WebSocketClosedError
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
tornado.websocket.WebSocketClosedError
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
tornado.websocket.WebSocketClosedError
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
tornado.websocket.WebSocketClosedError
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
tornado.websocket.WebSocketClosedError
[::1]:39066: client disconnect
[::1]:39068: client disconnect
[::1]:39070: client disconnect
[::1]:39074: client disconnect
[::1]:39076: client disconnect
Traceback (most recent call last):

  File "tornado/websocket.py", line 1102, in wrapper

tornado.iostream.StreamClosedError: Stream is closed


During handling of the above exception, another exception occurred:


Traceback (most recent call last):

  File "tornado/websocket.py", line 1104, in wrapper

tornado.websocket.WebSocketClosedError

Please lodge a bug report at:
	https://github.com/mitmproxy/mitmproxy/issues
```

I think this is a matter of mitmweb not properly keeping track of WebSockets and trying to write to closed ones.
After doing some digging this might as well be caused by an upstream bug https://github.com/tornadoweb/tornado/issues/2958 which would cause the WebSocket to never be removed from the set:

https://github.com/mitmproxy/mitmproxy/blob/73c38f698d42002947e46294498b2166f3139cf4/mitmproxy/tools/web/app.py#L275-L283
Honestly it feels like we should just swallow the error in `broadcast` [here](https://github.com/mitmproxy/mitmproxy/blob/73c38f698d42002947e46294498b2166f3139cf4/mitmproxy/tools/web/app.py#L292-L295) and be done with it.
> Honestly it feels like we should just swallow the error in `broadcast` [here](https://github.com/mitmproxy/mitmproxy/blob/73c38f698d42002947e46294498b2166f3139cf4/mitmproxy/tools/web/app.py#L292-L295) and be done with it.

And also `if closedError: self.connections.remove(self)` :eyes: ?

But I think that would only get rid of the

```
ERROR:root:Error sending message
Traceback (most recent call last):
  File "mitmproxy/tools/web/app.py", line 292, in broadcast
  File "tornado/websocket.py", line 337, in write_message
```

and not the _uncaught_ exception? I don't know where that originates though.
Also seeing this if I make a large number of requests (like a load test).

Resolving patch:
diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py
--- a/mitmproxy/tools/web/app.py
+++ b/mitmproxy/tools/web/app.py
@@ -280,7 +280,7 @@ def open(self):
         self.connections.add(self)
 
     def on_close(self):
-        self.connections.remove(self)
+        self.connections.discard(self)
 
     @classmethod
     def broadcast(cls, **kwargs):
@@ -288,12 +288,16 @@ def broadcast(cls, **kwargs):
             "utf8", "surrogateescape"
         )
 
+        errored = []
         for conn in cls.connections:
             try:
                 if not conn.ws_connection.is_closing():
                     conn.write_message(message)
             except Exception:  # pragma: no cover
-                logging.error("Error sending message", exc_info=True)
+                logging.debug("Error sending WebSocket message.", exc_info=True)
+                errored.append(conn)  # workaround for https://github.com/tornadoweb/tornado/issues/2958
+        for conn in errored:
+            cls.connections.remove(conn)
 
 
 class ClientConnection(WebSocketEventBroadcaster):


Test patch: