diff --git a/litellm/constants.py b/litellm/constants.py index 25770d960bb..fa94fa77d21 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -389,6 +389,10 @@ AIOHTTP_CONNECTOR_LIMIT: Final = get_env_int("AIOHTTP_CONNECTOR_LIMIT", 1000) AIOHTTP_CONNECTOR_LIMIT_PER_HOST: Final = get_env_int("AIOHTTP_CONNECTOR_LIMIT_PER_HOST", 500) AIOHTTP_KEEPALIVE_TIMEOUT: Final = get_env_int("AIOHTTP_KEEPALIVE_TIMEOUT", 15) AIOHTTP_TTL_DNS_CACHE: Final = get_env_int("AIOHTTP_TTL_DNS_CACHE", 300) +# Raised above aiohttp's 8190 default so large response headers (e.g. GitHub CSP on OAuth +# token exchange) parse instead of raising LineTooLong. https://github.com/BerriAI/litellm/issues/48468 +AIOHTTP_MAX_FIELD_SIZE: Final = get_env_int("AIOHTTP_MAX_FIELD_SIZE", 32768) +AIOHTTP_MAX_LINE_SIZE: Final = get_env_int("AIOHTTP_MAX_LINE_SIZE", 32768) # TCP keep-alive (SO_KEEPALIVE) — opt-in. Required when running behind NAT/LBs # whose idle timeout is shorter than provider response timeouts (e.g. AWS NAT # Gateway: 350s vs OpenAI/Azure: 600s). Without this, the kernel sends nothing diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index 0b64885df45..8081705a2c6 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -29,6 +29,8 @@ from litellm.constants import ( AIOHTTP_CONNECTOR_LIMIT, AIOHTTP_CONNECTOR_LIMIT_PER_HOST, AIOHTTP_KEEPALIVE_TIMEOUT, + AIOHTTP_MAX_FIELD_SIZE, + AIOHTTP_MAX_LINE_SIZE, AIOHTTP_NEEDS_CLEANUP_CLOSED, AIOHTTP_SO_KEEPALIVE, AIOHTTP_TCP_KEEPCNT, @@ -1351,6 +1353,8 @@ class AsyncHTTPHandler: connector=TCPConnector(**transport_connector_kwargs), cookie_jar=DummyCookieJar(), trust_env=trust_env, + max_field_size=AIOHTTP_MAX_FIELD_SIZE, + max_line_size=AIOHTTP_MAX_LINE_SIZE, ) # Use shared session if provided and valid diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index b263412ce0f..2cdc1d3d9ef 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -68,6 +68,8 @@ from litellm.constants import ( AIOHTTP_CONNECTOR_LIMIT, AIOHTTP_CONNECTOR_LIMIT_PER_HOST, AIOHTTP_KEEPALIVE_TIMEOUT, + AIOHTTP_MAX_FIELD_SIZE, + AIOHTTP_MAX_LINE_SIZE, AIOHTTP_NEEDS_CLEANUP_CLOSED, AIOHTTP_TTL_DNS_CACHE, AUDIO_SPEECH_CHUNK_SIZE, @@ -1410,7 +1412,12 @@ async def initialize_shared_aiohttp_session() -> "ClientSession | None": connector_kwargs["socket_factory"] = socket_factory connector: Final = TCPConnector(**connector_kwargs) - session: Final = ClientSession(connector=connector, cookie_jar=DummyCookieJar()) + session: Final = ClientSession( + connector=connector, + cookie_jar=DummyCookieJar(), + max_field_size=AIOHTTP_MAX_FIELD_SIZE, + max_line_size=AIOHTTP_MAX_LINE_SIZE, + ) verbose_proxy_logger.info( "SESSION REUSE: Created shared aiohttp session for connection pooling (ID: %s, limit=%s, limit_per_host=%s)", diff --git a/tests/unit/llms/custom_httpx/test_aiohttp_cleanup_closed.py b/tests/unit/llms/custom_httpx/test_aiohttp_cleanup_closed.py index 03f83903e55..ad6e38675dd 100644 --- a/tests/unit/llms/custom_httpx/test_aiohttp_cleanup_closed.py +++ b/tests/unit/llms/custom_httpx/test_aiohttp_cleanup_closed.py @@ -37,3 +37,24 @@ def test_create_aiohttp_transport_omits_enable_cleanup_closed_when_not_needed( transport.get_valid_client_session() assert "enable_cleanup_closed" not in mock_tcp_connector.call_args.kwargs + + +def test_create_aiohttp_transport_raises_header_size_limits(): + from litellm.constants import AIOHTTP_MAX_FIELD_SIZE, AIOHTTP_MAX_LINE_SIZE + from litellm.llms.custom_httpx import http_handler as http_handler_module + + connector_mock = MagicMock(name="connector") + session_mock = MagicMock(name="session") + + with patch.object(http_handler_module, "TCPConnector", return_value=connector_mock): + with ( + patch.object(http_handler_module, "ClientSession", return_value=session_mock) as mock_session, + patch.object(http_handler_module, "DummyCookieJar", return_value=MagicMock(name="cookie_jar")), + ): + transport = http_handler_module.AsyncHTTPHandler.create_aiohttp_transport(shared_session=None) + transport.get_valid_client_session() + + assert mock_session.call_args.kwargs["max_field_size"] == AIOHTTP_MAX_FIELD_SIZE + assert mock_session.call_args.kwargs["max_line_size"] == AIOHTTP_MAX_LINE_SIZE + assert AIOHTTP_MAX_FIELD_SIZE > 8190 + assert AIOHTTP_MAX_LINE_SIZE > 8190 diff --git a/tests/unit/proxy/test_aiohttp_cleanup_closed.py b/tests/unit/proxy/test_aiohttp_cleanup_closed.py index c9b4d6475ab..7217e02f9c5 100644 --- a/tests/unit/proxy/test_aiohttp_cleanup_closed.py +++ b/tests/unit/proxy/test_aiohttp_cleanup_closed.py @@ -11,9 +11,7 @@ def test_initialize_shared_aiohttp_session_sets_enable_cleanup_closed_when_neede session_mock = MagicMock(name="session") monkeypatch.setattr(proxy_server_module, "AIOHTTP_NEEDS_CLEANUP_CLOSED", True) - with patch( - "aiohttp.TCPConnector", return_value=connector_mock - ) as mock_tcp_connector: + with patch("aiohttp.TCPConnector", return_value=connector_mock) as mock_tcp_connector: with patch("aiohttp.ClientSession", return_value=session_mock): asyncio.run(proxy_server_module._initialize_shared_aiohttp_session()) @@ -29,10 +27,24 @@ def test_initialize_shared_aiohttp_session_omits_enable_cleanup_closed_when_not_ session_mock = MagicMock(name="session") monkeypatch.setattr(proxy_server_module, "AIOHTTP_NEEDS_CLEANUP_CLOSED", False) - with patch( - "aiohttp.TCPConnector", return_value=connector_mock - ) as mock_tcp_connector: + with patch("aiohttp.TCPConnector", return_value=connector_mock) as mock_tcp_connector: with patch("aiohttp.ClientSession", return_value=session_mock): asyncio.run(proxy_server_module._initialize_shared_aiohttp_session()) assert "enable_cleanup_closed" not in mock_tcp_connector.call_args.kwargs + + +def test_initialize_shared_aiohttp_session_raises_header_size_limits(): + from litellm.constants import AIOHTTP_MAX_FIELD_SIZE, AIOHTTP_MAX_LINE_SIZE + from litellm.proxy import proxy_server as proxy_server_module + + connector_mock = MagicMock(name="connector") + session_mock = MagicMock(name="session") + + with patch("aiohttp.TCPConnector", return_value=connector_mock): + with patch("aiohttp.ClientSession", return_value=session_mock) as mock_session: + asyncio.run(proxy_server_module._initialize_shared_aiohttp_session()) + + assert mock_session.call_args.kwargs["max_field_size"] == AIOHTTP_MAX_FIELD_SIZE + assert mock_session.call_args.kwargs["max_line_size"] == AIOHTTP_MAX_LINE_SIZE + assert AIOHTTP_MAX_FIELD_SIZE > 8190