Repository: mitmproxy/mitmproxy

Issue:
`test_fuzz_cancel` crashes for some inputs
From https://github.com/mitmproxy/mitmproxy/runs/6411486204:

```python
_______________________________ test_fuzz_cancel _______________________________

    @given(stream_request=booleans(), stream_response=booleans(), data=data())
>   def test_fuzz_cancel(stream_request, stream_response, data):

test/mitmproxy/proxy/layers/http/test_http_fuzz.py:420: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
test/mitmproxy/proxy/layers/http/test_http_fuzz.py:421: in test_fuzz_cancel
    _test_cancel(
test/mitmproxy/proxy/layers/http/test_http_fuzz.py:526: in _test_cancel
    assert playbook >> evt
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <test.mitmproxy.proxy.tutils.Playbook object at 0x1094a4ac0>

    def __bool__(self):
        """Determine if playbook is correct."""
        already_asserted = len(self.actual)
        i = already_asserted
        while i < len(self.expected):
            x = self.expected[i]
            if isinstance(x, commands.Command):
                pass
            else:
                if hasattr(x, "playbook_eval"):
                    try:
                        x = self.expected[i] = x.playbook_eval(self)
                    except Exception:
                        self.actual.append(_TracebackInPlaybook(traceback.format_exc()))
                        break
                for name, value in vars(x).items():
                    if isinstance(value, _Placeholder):
                        setattr(x, name, value())
                if isinstance(x, events.OpenConnectionCompleted) and not x.reply:
                    x.command.connection.state = ConnectionState.OPEN
                    x.command.connection.timestamp_start = 1624544785
                elif isinstance(x, events.ConnectionClosed):
                    x.connection.state &= ~ConnectionState.CAN_READ
                    x.connection.timestamp_end = 1624544787
    
                self.actual.append(x)
                cmds: list[commands.Command] = []
                try:
                    # consume them one by one so that we can extend the log with all commands until traceback.
                    for cmd in self.layer.handle_event(x):
                        cmds.append(cmd)
                except Exception:
                    self.actual.extend(cmds)
                    self.actual.append(_TracebackInPlaybook(traceback.format_exc()))
                    break
    
                cmds = list(
                    _merge_sends(
                        cmds, ignore_hooks=not self.hooks, ignore_logs=not self.logs
                    )
                )
    
                self.actual.extend(cmds)
                pos = len(self.actual) - len(cmds) - 1
                hook_replies = []
                for cmd in cmds:
                    pos += 1
                    assert self.actual[pos] == cmd
                    if isinstance(cmd, commands.CloseConnection):
                        if cmd.half_close:
                            cmd.connection.state &= ~ConnectionState.CAN_WRITE
                        else:
                            cmd.connection.state = ConnectionState.CLOSED
                    elif isinstance(cmd, commands.Log):
                        need_to_emulate_log = (
                            not self.logs
                            and cmd.level in ("debug", "info")
                            and (
                                pos >= len(self.expected)
                                or not isinstance(self.expected[pos], commands.Log)
                            )
                        )
                        if need_to_emulate_log:
                            self.expected.insert(pos, cmd)
                    elif isinstance(cmd, commands.StartHook) and not self.hooks:
                        need_to_emulate_hook = not self.hooks and (
                            pos >= len(self.expected)
                            or (
                                not (
                                    isinstance(self.expected[pos], commands.StartHook)
                                    and self.expected[pos].name == cmd.name
                                )
                            )
                        )
                        if need_to_emulate_hook:
                            self.expected.insert(pos, cmd)
                            if cmd.blocking:
                                # the current event may still have yielded more events, so we need to insert
                                # the reply *after* those additional events.
                                hook_replies.append(events.HookCompleted(cmd))
                self.expected = (
                    self.expected[: pos + 1] + hook_replies + self.expected[pos + 1 :]
                )
    
                eq(
                    self.expected[i:], self.actual[i:]
                )  # compare now already to set placeholders
            i += 1
    
        if not eq(self.expected, self.actual):
            self._errored = True
            diffs = list(
                difflib.ndiff(
                    [_fmt_entry(x) for x in self.expected],
                    [_fmt_entry(x) for x in self.actual],
                )
            )
            if already_asserted:
                diffs.insert(already_asserted, "==== asserted until here ====")
            diff = "\n".join(diffs)
>           raise AssertionError(f"Playbook mismatch!\n{diff}")
E           AssertionError: Playbook mismatch!
E             >> Start({})
E             << SendData(client, b'\x00\x00*\x04\x00\x00\x00\x00\x00\x00\x01\x00\x00\x10\x00\x00\x02\x00\x00\x00\x00\x00\x04\x00\x00\xff\xff\x00\x05\x00\x00@\x00\x00\x08\x00\x00\x00\x00\x00\x03\x00\x00\x00d\x00\x06\x00\x01\x00\x00')
E             >> DataReceived(client, b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n')
E             >> DataReceived(client, b'\x00\x00\x00\x04\x01\x00\x00\x00\x00')
E             >> DataReceived(client, b'\x00\x00\r\x01\x04\x00\x00\x00\x01\x82\x86\x84A\x88/\x91\xd3]\x05\\\x87\xa7')
E             << HttpRequestHeadersHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  response = Response(200, no content)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=closed, alpn=h2)

E                  server_conn = Server(example.com:80, state=open, alpn=h2)>)
E             >> DataReceived(client, b'\x00\x00\x03\x00\x01\x00\x00\x00\x01foo')
E             >> Reply(HttpRequestHeadersHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  response = Response(200, no content)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=closed, alpn=h2)

E                  server_conn = Server(example.com:80, state=open, alpn=h2)>), None)
E             << OpenConnection({'connection': Server({'id': '…149a8d', 'address': ('example.com', 80), 'state': <ConnectionState.OPEN: 3>, 'transport_protocol': 'tcp', 'alpn': b'h2', 'timestamp_start': 1624544785})})
E             >> Reply(OpenConnection({'connection': Server({'id': '…149a8d', 'address': ('example.com', 80), 'state': <ConnectionState.OPEN: 3>, 'transport_protocol': 'tcp', 'alpn': b'h2', 'timestamp_start': 1624544785})}), None)
E           - << Log('Streaming request to example.com.', 'info')
E           + << SendData(server, b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n\x00\x00*\x04\x00\x00\x00\x00\x00\x00\x01\x00\x00\x10\x00\x00\x02\x00\x00\x00\x00\x00\x04\x00\x00\xff\xff\x00\x05\x00\x00@\x00\x00\x08\x00\x00\x00\x00\x00\x03\x00\x00\x00d\x00\x06\x00\x01\x00\x00\x00\x00\r\x01\x04\x00\x00\x00\x01\x82\x86\x84A\x88/\x91\xd3]\x05\\\x87\xa7\x00\x00\x03\x00\x00\x00\x00\x00\x01foo')
E             << Log('Streaming request to example.com.', 'info')
E             << HttpRequestHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  response = Response(200, no content)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=closed, alpn=h2)

E                  server_conn = Server(example.com:80, state=open, alpn=h2)>)
E             >> DataReceived(server, b'\x00\x00\x01\x01\x04\x00\x00\x00\x01\x88')
E             >> ConnectionClosed(connection=Client({'id': '…9d91e6', 'peername': ('client', 1234), 'sockname': ('127.0.0.1', 8080), 'timestamp_start': 1605699329, 'state': <ConnectionState.CLOSED: 0>, 'transport_protocol': 'tcp', 'alpn': b'h2', 'timestamp_end': 1624544787}))
E             << CloseConnection({'connection': Client({'id': '…9d91e6', 'peername': ('client', 1234), 'sockname': ('127.0.0.1', 8080), 'timestamp_start': 1605699329, 'state': <ConnectionState.CLOSED: 0>, 'transport_protocol': 'tcp', 'alpn': b'h2', 'timestamp_end': 1624544787}), 'half_close': False})
E             >> Reply(HttpRequestHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  response = Response(200, no content)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=closed, alpn=h2)

E                  server_conn = Server(example.com:80, state=open, alpn=h2)>), None)
E             << SendData(server, b'\x00\x00\x00\x00\x01\x00\x00\x00\x01')
E             << HttpResponseHeadersHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  response = Response(200, no content)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=closed, alpn=h2)

E                  server_conn = Server(example.com:80, state=open, alpn=h2)>)
E             >> Reply(HttpResponseHeadersHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  response = Response(200, no content)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=closed, alpn=h2)

E                  server_conn = Server(example.com:80, state=open, alpn=h2)>), None)
E             << HttpErrorHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  response = Response(200, no content)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=closed, alpn=h2)

E                  server_conn = Server(example.com:80, state=open, alpn=h2)>)
E           ==== asserted until here ====
E             >> Reply(HttpErrorHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  response = Response(200, no content)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=closed, alpn=h2)

E                  server_conn = Server(example.com:80, state=open, alpn=h2)>), None)
E             >> DataReceived(server, b'\x00\x00\x03\x00\x01\x00\x00\x00\x01bar')
E           + << Traceback (most recent call last):
E                  File "/Users/runner/work/mitmproxy/mitmproxy/mitmproxy/proxy/layers/http/__init__.py", line 911, in event_to_child
E                    stream = self.streams[command.event.stream_id]
E                KeyError: 1
E           
E                During handling of the above exception, another exception occurred:
E           
E                Traceback (most recent call last):
E                  File "/Users/runner/work/mitmproxy/mitmproxy/test/mitmproxy/proxy/tutils.py", line 206, in __bool__
E                    for cmd in self.layer.handle_event(x):
E                  File "/Users/runner/work/mitmproxy/mitmproxy/mitmproxy/proxy/layer.py", line 143, in handle_event
E                    command = command_generator.send(send)
E                  File "/Users/runner/work/mitmproxy/mitmproxy/mitmproxy/proxy/layers/http/__init__.py", line 890, in _handle_event
E                    yield from self.event_to_child(handler, event)
E                  File "/Users/runner/work/mitmproxy/mitmproxy/mitmproxy/proxy/layers/http/__init__.py", line 914, in event_to_child
E                    assert isinstance(
E                AssertionError

test/mitmproxy/proxy/tutils.py:277: AssertionError
---------------------------------- Hypothesis ----------------------------------
Falsifying example: test_fuzz_cancel(
    stream_request=True, stream_response=False, data=data(...),
)
Draw 1: ('data_req', DataReceived(client, b'\x00\x00\r\x01\x04\x00\x00\x00\x01\x82\x86\x84A\x88/\x91\xd3]\x05\\\x87\xa7'))
Draw 2: ('data_reqbody', DataReceived(client, b'\x00\x00\x03\x00\x01\x00\x00\x00\x01foo'))
Draw 3: ('reply_hook_req_headers', reply({'args': (), 'to': HttpRequestHeadersHook(flow=Placeholder:<HTTPFlow
  request = Request(GET example.com:80/)
  client_conn = Client(client:1234, state=open, alpn=h2)
  server_conn = Server(<no address>, state=closed)>), 'side_effect': <function _test_cancel.<locals>.maybe_stream at 0x1091a3ac0>}))
Draw 4: ('reply_openconn', reply({'args': (None,), 'to': OpenConnection({'connection': Placeholder:Server({'id': '…149a8d', 'address': ('example.com', 80), 'state': <ConnectionState.CLOSED: 0>, 'transport_protocol': 'tcp'})}), 'side_effect': <function make_h2 at 0x10878ac20>}))
Draw 5: ('data_resp', DataReceived(_placeholder, b'\x00\x00\x01\x01\x04\x00\x00\x00\x01\x88'))
Draw 6: ('err_client_disc', ConnectionClosed(connection=Client({'id': '…9d91e6', 'peername': ('client', 1234), 'sockname': ('127.0.0.1', 8080), 'timestamp_start': [1605](https://github.com/mitmproxy/mitmproxy/runs/6411486204?check_suite_focus=true#step:6:1605)699329, 'state': <ConnectionState.OPEN: 3>, 'transport_protocol': 'tcp', 'alpn': b'h2'})))
Draw 7: ('reply_hook_req', reply({'args': (), 'to': HttpRequestHook(flow=Placeholder:<HTTPFlow
  request = Request(GET example.com:80/)
  client_conn = Client(client:1234, state=closed, alpn=h2)
  server_conn = Server(example.com:80, state=open, alpn=h2)>), 'side_effect': <function reply.<lambda> at 0x1086a7010>}))
Draw 8: ('reply_hook_resp_headers', reply({'args': (), 'to': HttpResponseHeadersHook(flow=Placeholder:<HTTPFlow
  request = Request(GET example.com:80/)
  response = Response(200, no content)
  client_conn = Client(client:1234, state=closed, alpn=h2)
  server_conn = Server(example.com:80, state=open, alpn=h2)>), 'side_effect': <function _test_cancel.<locals>.maybe_stream at 0x1091a3ac0>}))
Draw 9: ('reply_hook_error', reply({'args': (), 'to': HttpErrorHook(flow=Placeholder:<HTTPFlow
  request = Request(GET example.com:80/)
  response = Response(200, no content)
  error = peer closed connection
  client_conn = Client(client:1234, state=closed, alpn=h2)
  server_conn = Server(example.com:80, state=open, alpn=h2)>), 'side_effect': <function reply.<lambda> at 0x1086a7010>}))
Draw 10: ('data_respbody', DataReceived(_placeholder, b'\x00\x00\x03\x00\x01\x00\x00\x00\x01bar'))
```


Discussion:
Another one in https://github.com/mitmproxy/mitmproxy/runs/6442413613:

```python
E           AssertionError: Playbook mismatch!
E             >> Start({})
E             << SendData(client, b'\x00\x00*\x04\x00\x00\x00\x00\x00\x00\x01\x00\x00\x10\x00\x00\x02\x00\x00\x00\x00\x00\x04\x00\x00\xff\xff\x00\x05\x00\x00@\x00\x00\x08\x00\x00\x00\x00\x00\x03\x00\x00\x00d\x00\x06\x00\x01\x00\x00')
E             >> DataReceived(client, b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n')
E             >> DataReceived(client, b'\x00\x00\x00\x04\x01\x00\x00\x00\x00')
E             >> DataReceived(client, b'\x00\x00\r\x01\x04\x00\x00\x00\x01\x82\x86\x84A\x88/\x91\xd3]\x05\\\x87\xa7')
E             << HttpRequestHeadersHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=open, alpn=h2)

E                  server_conn = Server(example.com:80, state=closed, alpn=h2)>)
E             >> Reply(HttpRequestHeadersHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=open, alpn=h2)

E                  server_conn = Server(example.com:80, state=closed, alpn=h2)>), None)
E             << OpenConnection({'connection': Server({'id': '…75c3d4', 'address': ('example.com', 80), 'state': <ConnectionState.CLOSED: 0>, 'transport_protocol': 'tcp', 'alpn': b'h2', 'timestamp_start': 1624544785, 'timestamp_end': 1624544787})})
E             >> Reply(OpenConnection({'connection': Server({'id': '…75c3d4', 'address': ('example.com', 80), 'state': <ConnectionState.CLOSED: 0>, 'transport_protocol': 'tcp', 'alpn': b'h2', 'timestamp_start': 1624544785, 'timestamp_end': 1624544787})}), None)
E           - << Log('Streaming request to example.com.', 'info')
E           + << SendData(server, b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n\x00\x00*\x04\x00\x00\x00\x00\x00\x00\x01\x00\x00\x10\x00\x00\x02\x00\x00\x00\x00\x00\x04\x00\x00\xff\xff\x00\x05\x00\x00@\x00\x00\x08\x00\x00\x00\x00\x00\x03\x00\x00\x00d\x00\x06\x00\x01\x00\x00\x00\x00\r\x01\x04\x00\x00\x00\x01\x82\x86\x84A\x88/\x91\xd3]\x05\\\x87\xa7')
E             << Log('Streaming request to example.com.', 'info')
E             >> ConnectionClosed(connection=Server({'id': '…75c3d4', 'address': ('example.com', 80), 'state': <ConnectionState.CLOSED: 0>, 'transport_protocol': 'tcp', 'alpn': b'h2', 'timestamp_start': 1624544785, 'timestamp_end': 1624544787}))
E             << CloseConnection({'connection': Server({'id': '…75c3d4', 'address': ('example.com', 80), 'state': <ConnectionState.CLOSED: 0>, 'transport_protocol': 'tcp', 'alpn': b'h2', 'timestamp_start': 1624544785, 'timestamp_end': 1624544787}), 'half_close': False})
E             << HttpErrorHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=open, alpn=h2)

E                  server_conn = Server(example.com:80, state=closed, alpn=h2)>)
E             >> Reply(HttpErrorHook(flow=<HTTPFlow

E                  request = Request(GET example.com:80/)

E                  error = peer closed connection

E                  client_conn = Client(client:1234, state=open, alpn=h2)

E                  server_conn = Server(example.com:80, state=closed, alpn=h2)>), None)
E           ==== asserted until here ====
E             << SendData(client, b'\x00\x00\x1d\x01\x04\x00\x00\x00\x01H\x82l\x02v\x8e\xa4\xc9\xa6\xbb\x0f\xe7\xd2\x8f\xae\x05\xc0\xbc\x85\xef_\x87I|\xa5\x89\xd3M\x1f\x00\x00\x8e\x00\x01\x00\x00\x00\x01<html>\n<head>\n    <title>502 Bad Gateway</title>\n</head>\n<body>\n    <h1>502 Bad Gateway</h1>\n    <p>peer closed connection</p>\n</body>\n</html>')
E             >> DataReceived(client, b'\x00\x00\x03\x00\x01\x00\x00\x00\x01foo')
E           + << Traceback (most recent call last):
E                  File "/home/runner/work/mitmproxy/mitmproxy/mitmproxy/proxy/layers/http/__init__.py", line 911, in event_to_child
E                    stream = self.streams[command.event.stream_id]
E                KeyError: 1
E           
E                During handling of the above exception, another exception occurred:
E           
E                Traceback (most recent call last):
E                  File "/home/runner/work/mitmproxy/mitmproxy/test/mitmproxy/proxy/tutils.py", line 206, in __bool__
E                    for cmd in self.layer.handle_event(x):
E                  File "/home/runner/work/mitmproxy/mitmproxy/mitmproxy/proxy/layer.py", line 143, in handle_event
E                    command = command_generator.send(send)
E                  File "/home/runner/work/mitmproxy/mitmproxy/mitmproxy/proxy/layers/http/__init__.py", line 890, in _handle_event
E                    yield from self.event_to_child(handler, event)
E                  File "/home/runner/work/mitmproxy/mitmproxy/mitmproxy/proxy/layers/http/__init__.py", line 914, in event_to_child
E                    assert isinstance(
E                AssertionError

test/mitmproxy/proxy/tutils.py:277: AssertionError
---------------------------------- Hypothesis ----------------------------------
Falsifying example: test_fuzz_cancel(
    stream_request=True, stream_response=False, data=data(...),
)
Draw 1: ('data_req', DataReceived(client, b'\x00\x00\r\x01\x04\x00\x00\x00\x01\x82\x86\x84A\x88/\x91\xd3]\x05\\\x87\xa7'))
Draw 2: ('reply_hook_req_headers', reply({'args': (), 'to': HttpRequestHeadersHook(flow=Placeholder:<HTTPFlow
  request = Request(GET example.com:80/)
  client_conn = Client(client:1234, state=open, alpn=h2)
  server_conn = Server(<no address>, state=closed)>), 'side_effect': <function _test_cancel.<locals>.maybe_stream at 0x7f35d4734040>}))
Draw 3: ('reply_openconn', reply({'args': (None,), 'to': OpenConnection({'connection': Placeholder:Server({'id': '…75c3d4', 'address': ('example.com', 80), 'state': <ConnectionState.CLOSED: 0>, 'transport_protocol': 'tcp'})}), 'side_effect': <function make_h2 at 0x7f35d5c13b80>}))
Draw 4: ('err_server_disc', ConnectionClosed(connection=Placeholder:Server({'id': '…75c3d4', 'address': ('example.com', 80), 'state': <ConnectionState.OPEN: 3>, 'transport_protocol': 'tcp', 'alpn': b'h2', 'timestamp_start': 1624544785})))
Draw 5: ('reply_hook_error', reply({'args': (), 'to': HttpErrorHook(flow=Placeholder:<HTTPFlow
  request = Request(GET example.com:80/)
  error = peer closed connection
  client_conn = Client(client:1234, state=open, alpn=h2)
  server_conn = Server(example.com:80, state=closed, alpn=h2)>), 'side_effect': <function reply.<lambda> at 0x7f35d5f469d0>}))
Draw 6: ('data_reqbody', DataReceived(client, b'\x00\x00\x03\x00\x01\x00\x00\x00\x01foo'))
```

Resolving patch:
diff --git a/mitmproxy/proxy/layers/http/__init__.py b/mitmproxy/proxy/layers/http/__init__.py
--- a/mitmproxy/proxy/layers/http/__init__.py
+++ b/mitmproxy/proxy/layers/http/__init__.py
@@ -910,10 +910,9 @@ def event_to_child(
                 try:
                     stream = self.streams[command.event.stream_id]
                 except KeyError:
-                    # We may be getting errors for a specific stream even though we've already finished handling it.
-                    assert isinstance(
-                        command.event, (RequestProtocolError, ResponseProtocolError)
-                    )
+                    # We may be getting data or errors for a stream even though we've already finished handling it,
+                    # see for example https://github.com/mitmproxy/mitmproxy/issues/5343.
+                    pass
                 else:
                     yield from self.event_to_child(stream, command.event)
             elif isinstance(command, SendHttp):


Test patch: