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
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ dev

**Bugfixes**

- Preserve header fields when copying or pickling ``HeaderTuple`` and
``NeverIndexedHeaderTuple`` objects.


4.2.0 (2026-06-22)
------------------
Expand Down
3 changes: 3 additions & 0 deletions src/hpack/struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class HeaderTuple(tuple[bytes, bytes]):
def __new__(cls, *args: Any) -> Self:
return tuple.__new__(cls, args)

def __getnewargs__(self) -> tuple[bytes, ...]:
return tuple(self)


class NeverIndexedHeaderTuple(HeaderTuple):
"""
Expand Down
44 changes: 43 additions & 1 deletion tests/test_struct.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import copy
import pickle

import pytest

from hpack import HeaderTuple, NeverIndexedHeaderTuple
from hpack import Decoder, Encoder, HeaderTuple, NeverIndexedHeaderTuple


class TestHeaderTuple:
Expand Down Expand Up @@ -68,3 +71,42 @@ def test_equal_for_different_indexes(self):

assert t1 == t2
assert t1 is not t2

@pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple))
@pytest.mark.parametrize('copier', (copy.copy, copy.deepcopy))
def test_copy_preserves_header(self, cls, copier):
"""
Copying a header preserves its fields and concrete type.
"""
header = cls(b'name', b'\xff')

restored = copier(header)

assert restored == header
assert type(restored) is cls
assert restored.indexable == header.indexable

@pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple))
@pytest.mark.parametrize('protocol', range(pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_preserves_header(self, cls, protocol):
"""
Every pickle protocol preserves the fields and concrete type.
"""
header = cls(b'name', b'\xff')

restored = pickle.loads(pickle.dumps(header, protocol=protocol))

assert restored == header
assert type(restored) is cls
assert restored.indexable == header.indexable

@pytest.mark.parametrize('raw', (False, True))
def test_deepcopied_decoded_headers_can_be_encoded(self, raw):
"""
A copied decoder result can be passed back to the encoder.
"""
headers = Decoder().decode(Encoder().encode([('x-demo', 'value')]), raw=raw)

encoded = Encoder().encode(copy.deepcopy(headers))

assert Decoder().decode(encoded, raw=raw) == headers