fix(anthropic): stop leaked VCR httpx patch from failing live tests

vcrpy skips its httpx un-patch step when saving a cassette raises during
teardown, leaving httpx.HTTPTransport patched for the rest of the worker
session. Later live integration tests then replay against the wrong
cassette and fail with CannotOverwriteExistingCassetteException /
APIConnectionError. Add an autouse fixture that restores pristine httpx
transports around non-VCR tests so a leak can't cascade.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daughertyandopen-swe[bot] committed 2026-07-16 18:44:23 +00:00
1 parent 98216c0c1d
commit 440c0765b3
2 files changed
+111

No files matched your search

+45
View File
@@ -1,9 +1,54 @@
from collections.abc import Iterator
from typing import Any
import httpx
import pytest
from langchain_tests.conftest import CustomPersister, CustomSerializer, base_vcr_config
from vcr import VCR # type: ignore[import-untyped]
# Pristine, unpatched httpx transport handlers captured at import time (before
# any cassette is installed). VCR monkeypatches these while a cassette is
# active and restores them on teardown -- unless saving the cassette raises,
# in which case `vcr.cassette.CassetteContextDecorator.__exit__` skips its
# un-patch step and leaves the transport patched. That leak then intercepts
# every later request in the same worker (e.g. live integration tests get a
# stale cassette and fail with confusing connection errors).
_PRISTINE_HTTPX_TRANSPORTS = {
(httpx.HTTPTransport, "handle_request"): httpx.HTTPTransport.handle_request,
(
httpx.AsyncHTTPTransport,
"handle_async_request",
): httpx.AsyncHTTPTransport.handle_async_request,
}
def _restore_httpx_transports() -> None:
"""Undo any VCR monkeypatching left behind on httpx transports."""
for (cls, attribute), original in _PRISTINE_HTTPX_TRANSPORTS.items():
if getattr(cls, attribute) is not original:
setattr(cls, attribute, original)
@pytest.fixture(autouse=True)
def _guard_httpx_transport_patches(
request: pytest.FixtureRequest,
) -> Iterator[None]:
"""Stop leaked VCR cassette patches from breaking live tests.
A `@pytest.mark.vcr` test whose cassette fails to save can leave httpx
patched for the rest of the worker session. Tests that hit the network
(those without a `vcr` marker) then replay against the wrong cassette and
fail. Restoring the pristine transports around every non-VCR test keeps
such a leak from cascading, without interfering with VCR's own patching
during cassette-backed tests.
"""
uses_vcr = request.node.get_closest_marker("vcr") is not None
if not uses_vcr:
_restore_httpx_transports()
yield
if not uses_vcr:
_restore_httpx_transports()
def remove_request_headers(request: Any) -> Any:
for k in request.headers:
@@ -0,0 +1,66 @@
"""Regression tests for the httpx transport guard in `tests/conftest.py`.
vcrpy leaves httpx patched if saving a cassette raises during teardown, which
otherwise leaks the patch into unrelated (live) tests running on the same
worker. The `_restore_httpx_transports` helper and its autouse fixture undo
that leak so it can't cascade.
"""
from pathlib import Path
import httpx
import vcr # type: ignore[import-untyped]
from vcr.persisters.filesystem import ( # type: ignore[import-untyped]
CassetteNotFoundError,
)
from tests.conftest import _restore_httpx_transports
class _BoomPersister:
"""Persister that fails to load and errors while saving a cassette."""
@staticmethod
def load_cassette(*_args: object, **_kwargs: object) -> tuple[list, list]:
raise CassetteNotFoundError
@staticmethod
def save_cassette(*_args: object, **_kwargs: object) -> None:
msg = "serialization boom"
raise RuntimeError(msg)
def _ok_response(_request: httpx.Request) -> httpx.Response:
return httpx.Response(200, text="ok")
def test_restore_httpx_transports_is_a_noop_when_unpatched() -> None:
original = httpx.HTTPTransport.handle_request
_restore_httpx_transports()
assert httpx.HTTPTransport.handle_request is original
def test_restore_httpx_transports_reverts_leaked_vcr_patch(tmp_path: Path) -> None:
original_sync = httpx.HTTPTransport.handle_request
original_async = httpx.AsyncHTTPTransport.handle_async_request
my_vcr = vcr.VCR(record_mode="once")
my_vcr.register_persister(_BoomPersister())
# Recording a new interaction makes the cassette dirty; the failing save
# then trips the vcrpy bug that skips un-patching httpx on teardown.
try:
with my_vcr.use_cassette(str(tmp_path / "leak.yaml")):
transport = httpx.MockTransport(_ok_response)
with httpx.Client(transport=transport) as client:
client.get("http://example.test/")
except RuntimeError:
pass
# The leak is real: httpx is still patched by VCR.
assert httpx.HTTPTransport.handle_request is not original_sync
_restore_httpx_transports()
assert httpx.HTTPTransport.handle_request is original_sync
assert httpx.AsyncHTTPTransport.handle_async_request is original_async