Skip to content

Commit 1c51ad1

Browse files
committed
fix(tests): clear vcrpy's sticky _was_iter flag so materialized bodies stay bytes
Actual root cause of the async image-edit cassette leak. The previous diagnostic run produced this dead giveaway: [vcr-episode-body-hash] ... episode[0]: body type='bytes_iterator' is not bytes/bytearray/str -- cannot hash [vcr-safe-body-matcher] request body mismatch body[a]: type='bytes_iterator' length=unknown sha256=N/A body[b]: type='bytes_iterator' length=unknown sha256=N/A Both sides of the matcher were ``bytes_iterator`` **after** the materializer had supposedly converted them to bytes. That made no sense until I read vcrpy's ``Request`` class. vcrpy's ``Request`` keeps two private flags that are set in ``__init__`` from the original body's type and **never cleared by the setter**: def __init__(self, method, uri, body, headers): self._was_file = hasattr(body, "read") self._was_iter = _is_nonsequence_iterator(body) ... @Property def body(self): if self._was_file: return BytesIO(self._body) if self._was_iter: return iter(self._body) return self._body @body.setter def body(self, value): if isinstance(value, str): value = value.encode("utf-8") self._body = value # <-- does NOT touch _was_iter / _was_file So when httpx's async transport hands vcrpy an iterator body, ``_was_iter`` becomes ``True`` and stays there forever. Even after ``_materialize_iterable_body`` writes plain bytes via ``request.body = out``, the next read of ``.body`` re-wraps the stored bytes in ``iter()`` -- producing a fresh ``bytes_iterator`` that compares unequal to any other ``bytes_iterator`` via object identity. The matcher missed every time, the cassette grew by one episode per run, and the persister saw the same iterator type when trying to hash the body for the diagnostic log. Fix: after writing the materialized bytes, also force ``_was_iter`` and ``_was_file`` to ``False``. vcrpy exposes no public API for this, so we touch the private flags directly -- acknowledged as a pragmatic test-only hack with a clear unit boundary (the only call site is ``_materialize_iterable_body``). Local repro reproduces the exact production setup: ``Request('POST', url, iter(b'multipart-content'), {})`` on two sides, runs the matcher, asserts HIT. Verified the matcher hits on identical content and still raises on differing content. Should be the last fix needed. Existing cassettes that contain oddly-shaped bodies (lists of int chunks, etc. from the previous ``_was_iter=True`` save path) still match because the materializer canonicalises both sides to bytes before comparison -- no fourth re-flush required.
1 parent 9e2e5b6 commit 1c51ad1

1 file changed

Lines changed: 21 additions & 0 deletions

File tree

tests/_vcr_conftest_common.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,27 @@ def _materialize_iterable_body(request) -> None:
677677
except (AttributeError, TypeError):
678678
pass
679679

680+
# vcrpy's ``Request`` keeps two internal flags - ``_was_iter`` and
681+
# ``_was_file`` - that are set in ``__init__`` based on the type
682+
# of the original body and never cleared by the setter. Their job
683+
# is to make the ``body`` *getter* re-wrap the stored value in
684+
# ``iter()`` or ``BytesIO()`` on every access, so callers that
685+
# expect a stream still get one even after the body has been
686+
# consumed once. The side effect is that even after we write
687+
# plain ``bytes`` back via ``request.body = out``, the next
688+
# access still returns ``iter(self._body)`` - which gives every
689+
# matcher comparison a fresh ``bytes_iterator`` and makes
690+
# ``body_a == body_b`` an object-identity check that can never
691+
# succeed. Touching the private flags is the only escape hatch;
692+
# vcrpy exposes no public API for resetting them. After this
693+
# point the body really is ``bytes`` from the getter's
694+
# perspective.
695+
for attr in ("_was_iter", "_was_file"):
696+
try:
697+
setattr(request, attr, False)
698+
except (AttributeError, TypeError):
699+
pass
700+
680701

681702
def _key_fingerprint_matcher(r1, r2) -> None:
682703
def _fp(req):

0 commit comments

Comments
 (0)