]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Remove ENABLE_CONNECT_PROTOCOL from HTTP/2 Settings (#373)
authorSeth Michael Larson <sethmichaellarson@gmail.com>
Sun, 22 Sep 2019 22:37:57 +0000 (17:37 -0500)
committerGitHub <noreply@github.com>
Sun, 22 Sep 2019 22:37:57 +0000 (17:37 -0500)
httpx/dispatch/http2.py
tests/dispatch/test_http2.py
tests/dispatch/utils.py

index b1efe6a223d321f7d534323196d544b511cdfdf6..361ddf9a445a1e19ea5c3856ef56456ef8de68bc 100644 (file)
@@ -3,6 +3,7 @@ import typing
 
 import h2.connection
 import h2.events
+from h2.settings import SettingCodes, Settings
 
 from ..concurrency.base import BaseEvent, BaseTCPStream, ConcurrencyBackend, TimeoutFlag
 from ..config import TimeoutConfig, TimeoutTypes
@@ -65,6 +66,28 @@ class HTTP2Connection:
         await self.stream.close()
 
     def initiate_connection(self) -> None:
+        # Need to set these manually here instead of manipulating via
+        # __setitem__() otherwise the H2Connection will emit SettingsUpdate
+        # frames in addition to sending the undesired defaults.
+        self.h2_state.local_settings = Settings(
+            client=True,
+            initial_values={
+                # Disable PUSH_PROMISE frames from the server since we don't do anything
+                # with them for now.  Maybe when we support caching?
+                SettingCodes.ENABLE_PUSH: 0,
+                # These two are taken from h2 for safe defaults
+                SettingCodes.MAX_CONCURRENT_STREAMS: 100,
+                SettingCodes.MAX_HEADER_LIST_SIZE: 65536,
+            },
+        )
+
+        # Some websites (*cough* Yahoo *cough*) balk at this setting being
+        # present in the initial handshake since it's not defined in the original
+        # RFC despite the RFC mandating ignoring settings you don't know about.
+        del self.h2_state.local_settings[
+            h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL
+        ]
+
         self.h2_state.initiate_connection()
         data_to_send = self.h2_state.data_to_send()
         self.stream.write_no_block(data_to_send)
@@ -88,7 +111,6 @@ class HTTP2Connection:
             f"target={request.url.full_path!r} "
             f"headers={headers!r}"
         )
-
         self.h2_state.send_headers(stream_id, headers)
         data_to_send = self.h2_state.data_to_send()
         await self.stream.write(data_to_send, timeout)
index c286592e043cc6a66f4922c95bf70b223a9f30ee..776ad8da396a480c380952c6646aa0cbe196e70b 100644 (file)
@@ -1,5 +1,9 @@
 import json
 
+import h2.connection
+import h2.events
+from h2.settings import SettingCodes
+
 from httpx import AsyncClient, Client, Response
 
 from .utils import MockHTTP2Backend
@@ -165,3 +169,38 @@ async def test_async_http2_reconnect(backend):
 
     assert response_2.status_code == 200
     assert json.loads(response_2.content) == {"method": "GET", "path": "/2", "body": ""}
+
+
+async def test_http2_settings_in_handshake(backend):
+    backend = MockHTTP2Backend(app=app, backend=backend)
+
+    async with AsyncClient(backend=backend) as client:
+        await client.get("http://example.org")
+
+    h2_conn = backend.server.conn
+
+    assert isinstance(h2_conn, h2.connection.H2Connection)
+    expected_settings = {
+        SettingCodes.HEADER_TABLE_SIZE: 4096,
+        SettingCodes.ENABLE_PUSH: 0,
+        SettingCodes.MAX_CONCURRENT_STREAMS: 100,
+        SettingCodes.INITIAL_WINDOW_SIZE: 65535,
+        SettingCodes.MAX_FRAME_SIZE: 16384,
+        SettingCodes.MAX_HEADER_LIST_SIZE: 65536,
+        # This one's here because h2 helpfully populates remote_settings
+        # with default values even if the peer doesn't send the setting.
+        SettingCodes.ENABLE_CONNECT_PROTOCOL: 0,
+    }
+    assert dict(h2_conn.remote_settings) == expected_settings
+
+    # We don't expect the ENABLE_CONNECT_PROTOCOL to be in the handshake
+    expected_settings.pop(SettingCodes.ENABLE_CONNECT_PROTOCOL)
+
+    assert len(backend.server.settings_changed) == 1
+    settings = backend.server.settings_changed[0]
+
+    assert isinstance(settings, h2.events.RemoteSettingsChanged)
+    assert len(settings.changed_settings) == len(expected_settings)
+    for setting_code, changed_setting in settings.changed_settings.items():
+        assert isinstance(changed_setting, h2.settings.ChangedSetting)
+        assert changed_setting.new_value == expected_settings[setting_code]
index 38c1b24ec9d228fc5c335d80edd6f2267537a5cc..0798b31ff2b0d957bd3debd582df274ce7687e43 100644 (file)
@@ -41,6 +41,7 @@ class MockHTTP2Server(BaseTCPStream):
         self.close_connection = False
         self.return_data = {}
         self.returning = {}
+        self.settings_changed = []
 
     # TCP stream interface
 
@@ -81,6 +82,8 @@ class MockHTTP2Server(BaseTCPStream):
                 # This will throw an error if the event is for a not-yet created stream
                 elif self.returning[event.stream_id]:
                     self.send_return_data(event.stream_id)
+            elif isinstance(event, h2.events.RemoteSettingsChanged):
+                self.settings_changed.append(event)
 
     async def write(self, data: bytes, timeout) -> None:
         self.write_no_block(data)