From b850ed1188aced4e8c3cd7ef8b1a065fbc43edbb Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Tue, 28 Oct 2025 13:35:16 -0700 Subject: [PATCH 1/3] fix: prevent httpx DeprecationWarning memory leak in AsyncHTTPHandler Route bytes/str to content= parameter instead of data= to avoid deprecation warning that causes memory leak --- litellm/llms/custom_httpx/http_handler.py | 34 +++++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index accdddbc4ddf..7a7e63f4919f 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -301,17 +301,45 @@ async def post( if timeout is None: timeout = self.timeout + # ============================================================================ + # MEMORY LEAK FIX — Prevent httpx DeprecationWarning + # ============================================================================ + # Problem: + # httpx shows a DeprecationWarning when you pass bytes/str to `data=`. + # It wants you to use `content=` instead. + # + # Impact: + # The warning leaks memory. Preventing the warning fixes the leak. + # + # Fix: + # Move bytes/str from `data=` to `content=` before calling build_request. + # Keep dicts in `data=` (that's still correct). + # ============================================================================ + + request_data = None + request_content = content + + # Route data parameter to the correct httpx parameter based on type + if data is not None: + if isinstance(data, (bytes, str)): + # Bytes/strings belong in content= (only if not already provided) + if content is None: + request_content = data + else: + # dict/Mapping stays in data= parameter + request_data = data + req = self.client.build_request( "POST", url, - data=data, # type: ignore + data=request_data, json=json, params=params, headers=headers, timeout=timeout, files=files, - content=content, - ) + content=request_content, + ) response = await self.client.send(req, stream=stream) response.raise_for_status() return response From a8e830f28b4885f3ca1a9b45c7b633f44154b421 Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Tue, 28 Oct 2025 15:58:13 -0700 Subject: [PATCH 2/3] refactor: extract data/content preparation into helper function Create _prepare_request_data_and_content() helper to DRY up the logic for routing data/content parameters correctly in httpx requests. This helper prevents httpx DeprecationWarnings (which cause memory leaks) by moving bytes/str from data= to content= parameter while keeping dict/Mapping in data= parameter. Applied the helper consistently across all HTTP methods in both AsyncHTTPHandler and HTTPHandler classes: - post(), put(), patch(), delete() - single_connection_post_request() Related to: b850ed1188aced4e8c3cd7ef8b1a065fbc43edbb --- litellm/llms/custom_httpx/http_handler.py | 138 ++++++++++++++-------- 1 file changed, 92 insertions(+), 46 deletions(-) diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index 7a7e63f4919f..5b4001e4a47e 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -47,6 +47,46 @@ _DEFAULT_TIMEOUT = httpx.Timeout(timeout=5.0, connect=5.0) +def _prepare_request_data_and_content( + data: Optional[Union[dict, str, bytes]] = None, + content: Any = None, +) -> tuple[Optional[Union[dict, Mapping]], Any]: + """ + Helper function to route data/content parameters correctly for httpx requests + + This prevents httpx DeprecationWarnings that cause memory leaks. + + Background: + - httpx shows a DeprecationWarning when you pass bytes/str to `data=` + - It wants you to use `content=` instead for bytes/str + - The warning itself leaks memory when triggered repeatedly + + Solution: + - Move bytes/str from `data=` to `content=` before calling build_request + - Keep dicts in `data=` (that's still the correct parameter for dicts) + + Args: + data: Request data (can be dict, str, or bytes) + content: Request content (raw bytes/str) + + Returns: + Tuple of (request_data, request_content) properly routed for httpx + """ + request_data = None + request_content = content + + if data is not None: + if isinstance(data, (bytes, str)): + # Bytes/strings belong in content= (only if not already provided) + if content is None: + request_content = data + else: + # dict/Mapping stays in data= parameter + request_data = data + + return request_data, request_content + + def get_ssl_configuration( ssl_verify: Optional[VerifyTypes] = None, ) -> Union[bool, str, ssl.SSLContext]: @@ -301,33 +341,8 @@ async def post( if timeout is None: timeout = self.timeout - # ============================================================================ - # MEMORY LEAK FIX — Prevent httpx DeprecationWarning - # ============================================================================ - # Problem: - # httpx shows a DeprecationWarning when you pass bytes/str to `data=`. - # It wants you to use `content=` instead. - # - # Impact: - # The warning leaks memory. Preventing the warning fixes the leak. - # - # Fix: - # Move bytes/str from `data=` to `content=` before calling build_request. - # Keep dicts in `data=` (that's still correct). - # ============================================================================ - - request_data = None - request_content = content - - # Route data parameter to the correct httpx parameter based on type - if data is not None: - if isinstance(data, (bytes, str)): - # Bytes/strings belong in content= (only if not already provided) - if content is None: - request_content = data - else: - # dict/Mapping stays in data= parameter - request_data = data + # Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix) + request_data, request_content = _prepare_request_data_and_content(data, content) req = self.client.build_request( "POST", @@ -392,19 +407,23 @@ async def post( async def put( self, url: str, - data: Optional[Union[dict, str]] = None, # type: ignore + data: Optional[Union[dict, str, bytes]] = None, # type: ignore json: Optional[dict] = None, params: Optional[dict] = None, headers: Optional[dict] = None, timeout: Optional[Union[float, httpx.Timeout]] = None, stream: bool = False, + content: Any = None, ): try: if timeout is None: timeout = self.timeout + # Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix) + request_data, request_content = _prepare_request_data_and_content(data, content) + req = self.client.build_request( - "PUT", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore + "PUT", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore ) response = await self.client.send(req) response.raise_for_status() @@ -452,19 +471,23 @@ async def put( async def patch( self, url: str, - data: Optional[Union[dict, str]] = None, # type: ignore + data: Optional[Union[dict, str, bytes]] = None, # type: ignore json: Optional[dict] = None, params: Optional[dict] = None, headers: Optional[dict] = None, timeout: Optional[Union[float, httpx.Timeout]] = None, stream: bool = False, + content: Any = None, ): try: if timeout is None: timeout = self.timeout + # Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix) + request_data, request_content = _prepare_request_data_and_content(data, content) + req = self.client.build_request( - "PATCH", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore + "PATCH", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore ) response = await self.client.send(req) response.raise_for_status() @@ -512,18 +535,23 @@ async def patch( async def delete( self, url: str, - data: Optional[Union[dict, str]] = None, # type: ignore + data: Optional[Union[dict, str, bytes]] = None, # type: ignore json: Optional[dict] = None, params: Optional[dict] = None, headers: Optional[dict] = None, timeout: Optional[Union[float, httpx.Timeout]] = None, stream: bool = False, + content: Any = None, ): try: if timeout is None: timeout = self.timeout + + # Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix) + request_data, request_content = _prepare_request_data_and_content(data, content) + req = self.client.build_request( - "DELETE", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore + "DELETE", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore ) response = await self.client.send(req, stream=stream) response.raise_for_status() @@ -571,8 +599,11 @@ async def single_connection_post_request( Used for retrying connection client errors. """ + # Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix) + request_data, request_content = _prepare_request_data_and_content(data, content) + req = client.build_request( - "POST", url, data=data, json=json, params=params, headers=headers, content=content # type: ignore + "POST", url, data=request_data, json=json, params=params, headers=headers, content=request_content # type: ignore ) response = await client.send(req, stream=stream) response.raise_for_status() @@ -826,21 +857,24 @@ def post( logging_obj: Optional[LiteLLMLoggingObject] = None, ): try: + # Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix) + request_data, request_content = _prepare_request_data_and_content(data, content) + if timeout is not None: req = self.client.build_request( "POST", url, - data=data, # type: ignore + data=request_data, # type: ignore json=json, params=params, headers=headers, timeout=timeout, files=files, - content=content, # type: ignore + content=request_content, # type: ignore ) else: req = self.client.build_request( - "POST", url, data=data, json=json, params=params, headers=headers, files=files, content=content # type: ignore + "POST", url, data=request_data, json=json, params=params, headers=headers, files=files, content=request_content # type: ignore ) response = self.client.send(req, stream=stream) response.raise_for_status() @@ -868,21 +902,25 @@ def post( def patch( self, url: str, - data: Optional[Union[dict, str]] = None, + data: Optional[Union[dict, str, bytes]] = None, json: Optional[Union[dict, str]] = None, params: Optional[dict] = None, headers: Optional[dict] = None, stream: bool = False, timeout: Optional[Union[float, httpx.Timeout]] = None, + content: Any = None, ): try: + # Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix) + request_data, request_content = _prepare_request_data_and_content(data, content) + if timeout is not None: req = self.client.build_request( - "PATCH", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore + "PATCH", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore ) else: req = self.client.build_request( - "PATCH", url, data=data, json=json, params=params, headers=headers # type: ignore + "PATCH", url, data=request_data, json=json, params=params, headers=headers, content=request_content # type: ignore ) response = self.client.send(req, stream=stream) response.raise_for_status() @@ -911,21 +949,25 @@ def patch( def put( self, url: str, - data: Optional[Union[dict, str]] = None, + data: Optional[Union[dict, str, bytes]] = None, json: Optional[Union[dict, str]] = None, params: Optional[dict] = None, headers: Optional[dict] = None, stream: bool = False, timeout: Optional[Union[float, httpx.Timeout]] = None, + content: Any = None, ): try: + # Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix) + request_data, request_content = _prepare_request_data_and_content(data, content) + if timeout is not None: req = self.client.build_request( - "PUT", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore + "PUT", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore ) else: req = self.client.build_request( - "PUT", url, data=data, json=json, params=params, headers=headers # type: ignore + "PUT", url, data=request_data, json=json, params=params, headers=headers, content=request_content # type: ignore ) response = self.client.send(req, stream=stream) return response @@ -941,21 +983,25 @@ def put( def delete( self, url: str, - data: Optional[Union[dict, str]] = None, # type: ignore + data: Optional[Union[dict, str, bytes]] = None, # type: ignore json: Optional[dict] = None, params: Optional[dict] = None, headers: Optional[dict] = None, timeout: Optional[Union[float, httpx.Timeout]] = None, stream: bool = False, + content: Any = None, ): try: + # Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix) + request_data, request_content = _prepare_request_data_and_content(data, content) + if timeout is not None: req = self.client.build_request( - "DELETE", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore + "DELETE", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore ) else: req = self.client.build_request( - "DELETE", url, data=data, json=json, params=params, headers=headers # type: ignore + "DELETE", url, data=request_data, json=json, params=params, headers=headers, content=request_content # type: ignore ) response = self.client.send(req, stream=stream) response.raise_for_status() From a8e3261cd7f20029c2b1413b3a84ae33d45486bf Mon Sep 17 00:00:00 2001 From: AlexsanderHamir Date: Tue, 28 Oct 2025 17:36:14 -0700 Subject: [PATCH 3/3] fix: Python 3.8 compatibility - use Tuple instead of tuple in type hints Replace lowercase tuple[...] with typing.Tuple[...] in http_handler.py to fix 'TypeError: type object is not subscriptable' on Python 3.8 --- litellm/llms/custom_httpx/http_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index 5b4001e4a47e..cc451e5fa951 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -3,7 +3,7 @@ import ssl import sys import time -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Tuple, Union import certifi import httpx @@ -50,7 +50,7 @@ def _prepare_request_data_and_content( data: Optional[Union[dict, str, bytes]] = None, content: Any = None, -) -> tuple[Optional[Union[dict, Mapping]], Any]: +) -> Tuple[Optional[Union[dict, Mapping]], Any]: """ Helper function to route data/content parameters correctly for httpx requests