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
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)
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)
import json
+import h2.connection
+import h2.events
+from h2.settings import SettingCodes
+
from httpx import AsyncClient, Client, Response
from .utils import MockHTTP2Backend
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]
self.close_connection = False
self.return_data = {}
self.returning = {}
+ self.settings_changed = []
# TCP stream interface
# 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)