Repository: mitmproxy/mitmproxy

Issue:
ProtocolException("Error in HTTP connection: Http2ZombieException('Connection already dead')")
#### Problem Description

The thumbnails on YouTube don't load after navigating around the page. I'm not sure what exactly is happening, it seems to be related to HTTP 2? See the attached video. Sorry for the video being such a hot mess. During recording it didn't happen after the first navigation (of course, why would it) and it took a moment to be reproducible. When I opened the image in a new tab it still didn't load. The second time I [alt+enter]ed it into a new tab.

In dev tools the requests appear as `(pending)`. So mitmproxy never tells Chrome that the request failed?

![Selection_561](https://user-images.githubusercontent.com/679144/72176316-69dbf300-33de-11ea-80e2-09fe60047eb7.png)


#### Steps to reproduce the behavior:
1. Start `mitmdump`
2. Open Chromium
3. Browse to www.youtube.com
4. Click on a video
5. Go back to the front page by clicking on the logo
6. Most thumbnails will not load (you might need to repeat 4+5). I assume it is related to the error in the title. No amount of waiting makes them load.

Now the question is: is YouTube or Chromium doing something funny or is it mitmproxy that has the issue?

#### System Information

```
Mitmproxy: 5.0.1
Python:    3.7.5
OpenSSL:   OpenSSL 1.1.0j  20 Nov 2018
Platform:  Linux-5.3.0-26-generic-x86_64-with-Ubuntu-19.10-eoan
```

#### Attachments
video: http://www.prinzhorn.me/mitmproxy/screencast.mp4
video of me hovering the thumbnails: http://www.prinzhorn.me/mitmproxy/screencast-hover.mp4
dump: http://www.prinzhorn.me/mitmproxy/youtube

ProtocolException("Error in HTTP connection: Http2ZombieException('Connection already dead')")
#### Problem Description

The thumbnails on YouTube don't load after navigating around the page. I'm not sure what exactly is happening, it seems to be related to HTTP 2? See the attached video. Sorry for the video being such a hot mess. During recording it didn't happen after the first navigation (of course, why would it) and it took a moment to be reproducible. When I opened the image in a new tab it still didn't load. The second time I [alt+enter]ed it into a new tab.

In dev tools the requests appear as `(pending)`. So mitmproxy never tells Chrome that the request failed?

![Selection_561](https://user-images.githubusercontent.com/679144/72176316-69dbf300-33de-11ea-80e2-09fe60047eb7.png)


#### Steps to reproduce the behavior:
1. Start `mitmdump`
2. Open Chromium
3. Browse to www.youtube.com
4. Click on a video
5. Go back to the front page by clicking on the logo
6. Most thumbnails will not load (you might need to repeat 4+5). I assume it is related to the error in the title. No amount of waiting makes them load.

Now the question is: is YouTube or Chromium doing something funny or is it mitmproxy that has the issue?

#### System Information

```
Mitmproxy: 5.0.1
Python:    3.7.5
OpenSSL:   OpenSSL 1.1.0j  20 Nov 2018
Platform:  Linux-5.3.0-26-generic-x86_64-with-Ubuntu-19.10-eoan
```

#### Attachments
video: http://www.prinzhorn.me/mitmproxy/screencast.mp4
video of me hovering the thumbnails: http://www.prinzhorn.me/mitmproxy/screencast-hover.mp4
dump: http://www.prinzhorn.me/mitmproxy/youtube



Discussion:
Thanks for the extensive write-up! The obvious workaround is to disable HTTP2 in mitmproxy, but we want to get that fixed anyways of course.

/cc @Kriechi 

(somewhat refs #1775)
I can reproduce the issue with Firefox on macOS as well.
This problem seems related to RESET_STREAM, when the browser sends a request, but a few milliseconds sends a RESET for that same stream. The server already responded with the full body. Then something stalls/breaks/blocks - I haven't found that last bit yet...
Found the underlying problem - it is in one of our dependencies that take care of the low-level heavy-lifting for HTTP/2.
https://github.com/python-hyper/hyper-h2/issues/1210
Thanks for the extensive write-up! The obvious workaround is to disable HTTP2 in mitmproxy, but we want to get that fixed anyways of course.

/cc @Kriechi 

(somewhat refs #1775)
I can reproduce the issue with Firefox on macOS as well.
This problem seems related to RESET_STREAM, when the browser sends a request, but a few milliseconds sends a RESET for that same stream. The server already responded with the full body. Then something stalls/breaks/blocks - I haven't found that last bit yet...
Found the underlying problem - it is in one of our dependencies that take care of the low-level heavy-lifting for HTTP/2.
https://github.com/python-hyper/hyper-h2/issues/1210

Resolving patch:
diff --git a/mitmproxy/proxy/protocol/http2.py b/mitmproxy/proxy/protocol/http2.py
--- a/mitmproxy/proxy/protocol/http2.py
+++ b/mitmproxy/proxy/protocol/http2.py
@@ -87,6 +87,18 @@ class Http2Layer(base.Layer):
         # mypy type hints
         client_conn: connections.ClientConnection = None
 
+    class H2ConnLogger:
+        def __init__(self, name, log):
+            self.name = name
+            self.log = log
+
+        def debug(self, fmtstr, *args):
+            msg = "H2Conn {}: {}".format(self.name, fmtstr % args)
+            self.log(msg, "debug")
+
+        def trace(self, fmtstr, *args):
+            pass
+
     def __init__(self, ctx, mode: str) -> None:
         super().__init__(ctx)
         self.mode = mode
@@ -98,7 +110,8 @@ def __init__(self, ctx, mode: str) -> None:
             client_side=False,
             header_encoding=False,
             validate_outbound_headers=False,
-            validate_inbound_headers=False)
+            validate_inbound_headers=False,
+            logger=self.H2ConnLogger("client", self.log))
         self.connections[self.client_conn] = SafeH2Connection(self.client_conn, config=config)
 
     def _initiate_server_conn(self):
@@ -107,7 +120,8 @@ def _initiate_server_conn(self):
                 client_side=True,
                 header_encoding=False,
                 validate_outbound_headers=False,
-                validate_inbound_headers=False)
+                validate_inbound_headers=False,
+                logger=self.H2ConnLogger("server", self.log))
             self.connections[self.server_conn] = SafeH2Connection(self.server_conn, config=config)
         self.connections[self.server_conn].initiate_connection()
         self.server_conn.send(self.connections[self.server_conn].data_to_send())
@@ -195,10 +209,12 @@ def _handle_data_received(self, eid, event, source_conn):
         else:
             self.streams[eid].data_queue.put(event.data)
             self.streams[eid].queued_data_length += len(event.data)
-            self.connections[source_conn].safe_acknowledge_received_data(
-                event.flow_controlled_length,
-                event.stream_id
-            )
+
+        # always acknowledge receved data with a WINDOW_UPDATE frame
+        self.connections[source_conn].safe_acknowledge_received_data(
+            event.flow_controlled_length,
+            event.stream_id
+        )
         return True
 
     def _handle_stream_ended(self, eid):
@@ -461,7 +477,7 @@ def raise_zombie(self, pre_command=None):  # pragma: no cover
         if self.zombie is not None or connection_closed:
             if pre_command is not None:
                 pre_command()
-            raise exceptions.Http2ZombieException("Connection already dead")
+            raise exceptions.Http2ZombieException("Connection or stream already dead: {}, {}".format(self.zombie, connection_closed))
 
     @detect_zombie_stream
     def read_request_headers(self, flow):
@@ -643,7 +659,8 @@ def run(self):
         try:
             layer()
         except exceptions.Http2ZombieException:  # pragma: no cover
-            pass
+            # zombies can be safely terminated - no need to kill them twice
+            return
         except exceptions.ProtocolException as e:  # pragma: no cover
             self.log(repr(e), "info")
         except exceptions.SetServerNotAllowedException as e:  # pragma: no cover
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -68,7 +68,7 @@
         "click>=7.0,<8",
         "cryptography>=2.1.4,<2.5",
         "flask>=1.1.1,<1.2",
-        "h2>=3.0.1,<4",
+        "h2>=3.2.0,<4",
         "hyperframe>=5.1.0,<6",
         "kaitaistruct>=0.7,<0.9",
         "ldap3>=2.6.1,<2.7",


Test patch: