Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ dev

**API Changes (Backward Compatible)**

- Encoding indexed header fields no longer allocates temporary buffers.

**Bugfixes**


Expand Down
4 changes: 4 additions & 0 deletions src/hpack/hpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
INDEX_NEVER = b"\x10"
INDEX_INCREMENTAL = b"\x40"

_INDEXED_BYTES = tuple(bytes([index | 0x80]) for index in range(127))

VARINT_MAX_LENGTH = 5 # octets, enough for encoding prefix + uint32

# Precompute 2^i for 1-8 for use in prefix calcs.
Expand Down Expand Up @@ -354,6 +356,8 @@ def _encode_indexed(self, index: int) -> bytes:
"""
Encodes a header using the indexed representation.
"""
if type(index) is int and 0 <= index < 127 and not log.isEnabledFor(logging.DEBUG):
return _INDEXED_BYTES[index]
field = encode_integer(index, 7)
field[0] |= 0x80 # we set the top bit
return bytes(field)
Expand Down
36 changes: 36 additions & 0 deletions tests/test_hpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,3 +856,39 @@ def _prepend_colon(k):

assert expected_special == received_special
assert expected_boring == received_boring


def test_debug_logging_preserves_roundtrip(caplog):
import logging
caplog.set_level(logging.DEBUG, logger='hpack')
encoder, decoder = Encoder(), Decoder()
encoder.header_table_size = 1024
headers = [(b':method', b'GET'), (b'custom-name', b'value'),
(b'authorization', b'private', True)]
for huffman in (False, True):
assert decoder.decode(encoder.encode(headers, huffman=huffman), raw=True) == [
(b':method', b'GET'), (b'custom-name', b'value'),
(b'authorization', b'private'),
]
messages = [record.getMessage() for record in caplog.records]
assert any('SENSITIVE_REDACTED' in message for message in messages)
encoding_records = [record for record in caplog.records if record.getMessage().startswith('Encoding ')]
assert encoding_records
assert all(record.funcName == 'encode_integer' for record in encoding_records)
assert any(message.startswith('Decoded ') for message in messages)


@pytest.mark.parametrize('index, wire', [
(0, b'\x80'), (1, b'\x81'), (126, b'\xfe'),
(127, b'\xff\x00'), (128, b'\xff\x01'), (255, b'\xff\x80\x01'),
(True, b'\x81'),
])
def test_indexed_encoding_at_prefix_boundary(index, wire):
assert Encoder()._encode_indexed(index) == wire


def test_indexed_encoding_invalid_values():
with pytest.raises(ValueError, match='Can only encode positive integers, got -1'):
Encoder()._encode_indexed(-1)
with pytest.raises(TypeError):
Encoder()._encode_indexed(1.5)