Skip to content

Commit a48a2fe

Browse files
committed
encode/httpx#1214 add an ability to send outbound cookies separately to improve headers compression
1 parent 9df8f94 commit a48a2fe

File tree

5 files changed

+89
-4
lines changed

5 files changed

+89
-4
lines changed

src/h2/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ class H2Configuration:
117117
RFC 7540. Defaults to ``True``.
118118
:type normalize_outbound_headers: ``bool``
119119
120+
:param split_outbound_cookies: Controls whether the outbound cookie
121+
headers are split before sending or not. According to RFC 7540
122+
- 8.1.2.5 the outbound header cookie headers may be split to improve
123+
headers compression. Default is ``False``.
124+
:type split_outbound_cookies: ``bool``
125+
120126
:param validate_inbound_headers: Controls whether the headers received
121127
by this object are validated against the rules in RFC 7540.
122128
Disabling this setting will cause inbound header validation to
@@ -148,6 +154,9 @@ class H2Configuration:
148154
normalize_outbound_headers = _BooleanConfigOption(
149155
'normalize_outbound_headers'
150156
)
157+
split_outbound_cookies = _BooleanConfigOption(
158+
'split_outbound_cookies'
159+
)
151160
validate_inbound_headers = _BooleanConfigOption(
152161
'validate_inbound_headers'
153162
)
@@ -160,13 +169,15 @@ def __init__(self,
160169
header_encoding=None,
161170
validate_outbound_headers=True,
162171
normalize_outbound_headers=True,
172+
split_outbound_cookies=False,
163173
validate_inbound_headers=True,
164174
normalize_inbound_headers=True,
165175
logger=None):
166176
self.client_side = client_side
167177
self.header_encoding = header_encoding
168178
self.validate_outbound_headers = validate_outbound_headers
169179
self.normalize_outbound_headers = normalize_outbound_headers
180+
self.split_outbound_cookies = split_outbound_cookies
170181
self.validate_inbound_headers = validate_inbound_headers
171182
self.normalize_inbound_headers = normalize_inbound_headers
172183
self.logger = logger or DummyLogger(__name__)

src/h2/stream.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1243,8 +1243,12 @@ def _build_headers_frames(self,
12431243
# We need to lowercase the header names, and to ensure that secure
12441244
# header fields are kept out of compression contexts.
12451245
if self.config.normalize_outbound_headers:
1246+
# also we may want to split outbound cookies to improve
1247+
# headers compression
1248+
should_split_outbound_cookies = self.config.split_outbound_cookies
1249+
12461250
headers = normalize_outbound_headers(
1247-
headers, hdr_validation_flags
1251+
headers, hdr_validation_flags, should_split_outbound_cookies
12481252
)
12491253
if self.config.validate_outbound_headers:
12501254
headers = validate_outbound_headers(

src/h2/utilities.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,14 +603,41 @@ def _combine_cookie_fields(headers, hdr_validation_flags):
603603
yield NeverIndexedHeaderTuple(b'cookie', cookie_val)
604604

605605

606-
def normalize_outbound_headers(headers, hdr_validation_flags):
606+
def _split_outbound_cookie_fields(headers, hdr_validation_flags):
607+
"""
608+
RFC 7540 § 8.1.2.5 allows for better compression efficiency,
609+
to split the Cookie header field into separate header fields
610+
611+
We want to do it for outbound requests, as we are doing for
612+
inbound.
613+
"""
614+
for header in headers:
615+
if header[0] in (b'cookie', 'cookie'):
616+
needle = b'; ' if isinstance(header[0], bytes) else '; '
617+
618+
if needle in header[1]:
619+
for cookie_val in header[1].split(needle):
620+
if isinstance(header, HeaderTuple):
621+
yield header.__class__(header[0], cookie_val)
622+
else:
623+
yield header[0], cookie_val
624+
else:
625+
yield header
626+
else:
627+
yield header
628+
629+
630+
def normalize_outbound_headers(headers, hdr_validation_flags, should_split_outbound_cookies):
607631
"""
608632
Normalizes a header sequence that we are about to send.
609633
610634
:param headers: The HTTP header set.
611635
:param hdr_validation_flags: An instance of HeaderValidationFlags.
636+
:param should_split_outbound_cookies: boolean flag
612637
"""
613638
headers = _lowercase_header_names(headers, hdr_validation_flags)
639+
if should_split_outbound_cookies:
640+
headers = _split_outbound_cookie_fields(headers, hdr_validation_flags)
614641
headers = _strip_surrounding_whitespace(headers, hdr_validation_flags)
615642
headers = _strip_connection_headers(headers, hdr_validation_flags)
616643
headers = _secure_headers(headers, hdr_validation_flags)

test/test_basic_logic.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import hyperframe
1111
import pytest
12+
from hpack import HeaderTuple
1213

1314
import h2.config
1415
import h2.connection
@@ -789,6 +790,48 @@ def test_headers_are_lowercase(self, frame_factory):
789790

790791
assert c.data_to_send() == expected_frame.serialize()
791792

793+
def test_outbound_cookie_headers_are_split(self):
794+
"""
795+
We should split outbound cookie headers according to
796+
RFC 7540 - 8.1.2.5
797+
"""
798+
cookie_headers = [
799+
HeaderTuple('cookie',
800+
'username=John Doe; expires=Thu, 18 Dec 2013 12:00:00 UTC'),
801+
('cookie', 'path=1'),
802+
('cookie', 'test1=val1; test2=val2')
803+
]
804+
805+
expected_cookie_headers = [
806+
HeaderTuple('cookie', 'username=John Doe'),
807+
HeaderTuple('cookie', 'expires=Thu, 18 Dec 2013 12:00:00 UTC'),
808+
('cookie', 'path=1'),
809+
('cookie', 'test1=val1'),
810+
('cookie', 'test2=val2'),
811+
]
812+
813+
client_config = h2.config.H2Configuration(
814+
client_side=True,
815+
header_encoding='utf-8',
816+
split_outbound_cookies=True
817+
)
818+
server_config = h2.config.H2Configuration(
819+
client_side=False,
820+
normalize_inbound_headers=False,
821+
header_encoding='utf-8'
822+
)
823+
client = h2.connection.H2Connection(config=client_config)
824+
server = h2.connection.H2Connection(config=server_config)
825+
826+
client.initiate_connection()
827+
client.send_headers(1, self.example_request_headers + cookie_headers, end_stream=True)
828+
829+
e = server.receive_data(client.data_to_send())
830+
831+
cookie_fields = [(n, v) for n, v in e[1].headers if n == 'cookie']
832+
833+
assert cookie_fields == expected_cookie_headers
834+
792835
@given(frame_size=integers(min_value=2**14, max_value=(2**24 - 1)))
793836
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
794837
def test_changing_max_frame_size(self, frame_factory, frame_size):

test/test_invalid_headers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def test_headers_event_skipping_validation(self, frame_factory, headers):
296296
c.send_headers(1, headers)
297297

298298
# Ensure headers are still normalized.
299-
norm_headers = h2.utilities.normalize_outbound_headers(headers, None)
299+
norm_headers = h2.utilities.normalize_outbound_headers(headers, None, False)
300300
f = frame_factory.build_headers_frame(norm_headers)
301301
assert c.data_to_send() == f.serialize()
302302

@@ -322,7 +322,7 @@ def test_push_promise_skipping_validation(self, frame_factory, headers):
322322

323323
# Create push promise frame with normalized headers.
324324
frame_factory.refresh_encoder()
325-
norm_headers = h2.utilities.normalize_outbound_headers(headers, None)
325+
norm_headers = h2.utilities.normalize_outbound_headers(headers, None, False)
326326
pp_frame = frame_factory.build_push_promise_frame(
327327
stream_id=1, promised_stream_id=2, headers=norm_headers
328328
)

0 commit comments

Comments
 (0)