diff --git a/pyproject.toml b/pyproject.toml index 1e499f295f4..50da6e297bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "tokenizers>=0.21.0,<1.0", "click>=8.0.0,<9.0", "jinja2>=3.1.6,<4.0", - "aiohttp>=3.10,<4.0", + "aiohttp>=3.14.2,<4.0", "pydantic>=2.10.0,<3.0.0", "jsonschema>=4.0.0,<5.0", ] @@ -233,7 +233,7 @@ build-backend = "uv_build" [tool.uv] constraint-dependencies = [ "tornado>=6.5.6", - "aiohttp>=3.14.1,<4.0", + "aiohttp>=3.14.2,<4.0", ] override-dependencies = [ "cryptography>=50.0.0,<51.0", diff --git a/tests/local_testing/test_basic_python_version.py b/tests/local_testing/test_basic_python_version.py index 8308e0d6033..c779a6dd475 100644 --- a/tests/local_testing/test_basic_python_version.py +++ b/tests/local_testing/test_basic_python_version.py @@ -201,3 +201,76 @@ def test_litellm_proxy_server_config_no_general_settings_v2_resolver(): with the v1 variant when they share a database. """ _run_proxy_server_smoke_test(extra_proxy_args=["--use_v2_migration_resolver"]) + + +AIOHTTP_POOL_POISONING_RANGE = ">=3.14.0,<3.14.2" +AIOHTTP_POOL_POISONING_RELEASES = ("3.14.0", "3.14.1") + + +def _load_toml(path): + try: + import tomllib as tomli + except ImportError: + try: + import tomli + except ImportError: + pytest.skip("tomli/tomllib not available - skipping dependency check") + + with open(path, "rb") as f: + return tomli.load(f) + + +def _declared_aiohttp_specifier(): + from packaging.requirements import Requirement + + pyproject = _load_toml(os.path.join(PROJECT_ROOT, "pyproject.toml")) + for requirement in pyproject["project"]["dependencies"]: + parsed = Requirement(requirement) + if parsed.name.lower() == "aiohttp": + return parsed.specifier + pytest.fail("aiohttp is no longer a declared runtime dependency of litellm") + + +def _locked_aiohttp_version(): + lock = _load_toml(os.path.join(PROJECT_ROOT, "uv.lock")) + for package in lock["package"]: + if package["name"].lower() == "aiohttp": + return package["version"] + pytest.fail("aiohttp is missing from uv.lock") + + +def test_declared_aiohttp_floor_excludes_pool_poisoning_releases(): + """aiohttp 3.14.0/3.14.1 re-arm the sock_read timer on a keep-alive connection + after it is back in the idle pool, so the next request to reuse it fails + instantly with a bogus timeout (aio-libs/aiohttp#12953, fixed in 3.14.2). + + The wheel's own metadata is what pip resolves against, so the floor declared + here - not just the lockfile - has to exclude that range. + """ + specifier = _declared_aiohttp_specifier() + + admitted = [v for v in AIOHTTP_POOL_POISONING_RELEASES if specifier.contains(v)] + assert not admitted, ( + f"litellm declares aiohttp{specifier}, which still admits {admitted}. " + "Those releases poison pooled keep-alive connections and cause " + "cross-provider sub-millisecond 'Connection timed out' failures; " + "keep the floor at >=3.14.2." + ) + + +def test_locked_aiohttp_version_is_not_pool_poisoning(): + """uv.lock is what the published Docker images install (uv sync --frozen), so a + lock that drifts back onto 3.14.0/3.14.1 ships the regression regardless of + what pyproject.toml declares. + """ + from packaging.specifiers import SpecifierSet + + locked = _locked_aiohttp_version() + + assert not SpecifierSet(AIOHTTP_POOL_POISONING_RANGE).contains(locked), ( + f"uv.lock resolves aiohttp {locked}, which is inside the pool-poisoning " + f"range {AIOHTTP_POOL_POISONING_RANGE} (aio-libs/aiohttp#12953). " + "Re-run `uv lock` against an aiohttp>=3.14.2 floor." + ) + + diff --git a/uv.lock b/uv.lock index 78a7b675edf..aa74f4a563f 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-08-05T07:40:35.209598Z" +exclude-newer = "2026-08-05T07:44:13.302644Z" exclude-newer-span = "P3D" [manifest] @@ -19,7 +19,7 @@ members = [ "litellm-proxy-extras", ] constraints = [ - { name = "aiohttp", specifier = ">=3.14.1,<4.0" }, + { name = "aiohttp", specifier = ">=3.14.2,<4.0" }, { name = "tornado", specifier = ">=6.5.6" }, ] overrides = [{ name = "cryptography", specifier = ">=50.0.0,<51.0" }] @@ -3468,7 +3468,7 @@ proxy-dev = [ [package.metadata] requires-dist = [ { name = "a2a-sdk", marker = "extra == 'extra-proxy'", specifier = ">=0.3.24,<1.0" }, - { name = "aiohttp", specifier = ">=3.10,<4.0" }, + { name = "aiohttp", specifier = ">=3.14.2,<4.0" }, { name = "anthropic", extras = ["vertex"], marker = "extra == 'proxy-runtime'", specifier = ">=0.84.0,<1.0" }, { name = "apscheduler", marker = "extra == 'proxy'", specifier = ">=3.11.2,<4.0" }, { name = "audioread", marker = "extra == 'stt-nvidia-riva'", specifier = ">=3.0.1" },