Skip to content

Commit 4939793

Browse files
fix: prevent httpx DeprecationWarning memory leak in AsyncHTTPHandler (#16024)
* 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 * 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: b850ed1 * 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
1 parent 5bba1e8 commit 4939793

File tree

1 file changed

+97
-23
lines changed

1 file changed

+97
-23
lines changed

litellm/llms/custom_httpx/http_handler.py

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import ssl
44
import sys
55
import time
6-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Union
6+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Tuple, Union
77

88
import certifi
99
import httpx
@@ -47,6 +47,46 @@
4747
_DEFAULT_TIMEOUT = httpx.Timeout(timeout=5.0, connect=5.0)
4848

4949

50+
def _prepare_request_data_and_content(
51+
data: Optional[Union[dict, str, bytes]] = None,
52+
content: Any = None,
53+
) -> Tuple[Optional[Union[dict, Mapping]], Any]:
54+
"""
55+
Helper function to route data/content parameters correctly for httpx requests
56+
57+
This prevents httpx DeprecationWarnings that cause memory leaks.
58+
59+
Background:
60+
- httpx shows a DeprecationWarning when you pass bytes/str to `data=`
61+
- It wants you to use `content=` instead for bytes/str
62+
- The warning itself leaks memory when triggered repeatedly
63+
64+
Solution:
65+
- Move bytes/str from `data=` to `content=` before calling build_request
66+
- Keep dicts in `data=` (that's still the correct parameter for dicts)
67+
68+
Args:
69+
data: Request data (can be dict, str, or bytes)
70+
content: Request content (raw bytes/str)
71+
72+
Returns:
73+
Tuple of (request_data, request_content) properly routed for httpx
74+
"""
75+
request_data = None
76+
request_content = content
77+
78+
if data is not None:
79+
if isinstance(data, (bytes, str)):
80+
# Bytes/strings belong in content= (only if not already provided)
81+
if content is None:
82+
request_content = data
83+
else:
84+
# dict/Mapping stays in data= parameter
85+
request_data = data
86+
87+
return request_data, request_content
88+
89+
5090
def get_ssl_configuration(
5191
ssl_verify: Optional[VerifyTypes] = None,
5292
) -> Union[bool, str, ssl.SSLContext]:
@@ -301,17 +341,20 @@ async def post(
301341
if timeout is None:
302342
timeout = self.timeout
303343

344+
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
345+
request_data, request_content = _prepare_request_data_and_content(data, content)
346+
304347
req = self.client.build_request(
305348
"POST",
306349
url,
307-
data=data, # type: ignore
350+
data=request_data,
308351
json=json,
309352
params=params,
310353
headers=headers,
311354
timeout=timeout,
312355
files=files,
313-
content=content,
314-
)
356+
content=request_content,
357+
)
315358
response = await self.client.send(req, stream=stream)
316359
response.raise_for_status()
317360
return response
@@ -364,19 +407,23 @@ async def post(
364407
async def put(
365408
self,
366409
url: str,
367-
data: Optional[Union[dict, str]] = None, # type: ignore
410+
data: Optional[Union[dict, str, bytes]] = None, # type: ignore
368411
json: Optional[dict] = None,
369412
params: Optional[dict] = None,
370413
headers: Optional[dict] = None,
371414
timeout: Optional[Union[float, httpx.Timeout]] = None,
372415
stream: bool = False,
416+
content: Any = None,
373417
):
374418
try:
375419
if timeout is None:
376420
timeout = self.timeout
377421

422+
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
423+
request_data, request_content = _prepare_request_data_and_content(data, content)
424+
378425
req = self.client.build_request(
379-
"PUT", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore
426+
"PUT", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore
380427
)
381428
response = await self.client.send(req)
382429
response.raise_for_status()
@@ -424,19 +471,23 @@ async def put(
424471
async def patch(
425472
self,
426473
url: str,
427-
data: Optional[Union[dict, str]] = None, # type: ignore
474+
data: Optional[Union[dict, str, bytes]] = None, # type: ignore
428475
json: Optional[dict] = None,
429476
params: Optional[dict] = None,
430477
headers: Optional[dict] = None,
431478
timeout: Optional[Union[float, httpx.Timeout]] = None,
432479
stream: bool = False,
480+
content: Any = None,
433481
):
434482
try:
435483
if timeout is None:
436484
timeout = self.timeout
437485

486+
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
487+
request_data, request_content = _prepare_request_data_and_content(data, content)
488+
438489
req = self.client.build_request(
439-
"PATCH", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore
490+
"PATCH", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore
440491
)
441492
response = await self.client.send(req)
442493
response.raise_for_status()
@@ -484,18 +535,23 @@ async def patch(
484535
async def delete(
485536
self,
486537
url: str,
487-
data: Optional[Union[dict, str]] = None, # type: ignore
538+
data: Optional[Union[dict, str, bytes]] = None, # type: ignore
488539
json: Optional[dict] = None,
489540
params: Optional[dict] = None,
490541
headers: Optional[dict] = None,
491542
timeout: Optional[Union[float, httpx.Timeout]] = None,
492543
stream: bool = False,
544+
content: Any = None,
493545
):
494546
try:
495547
if timeout is None:
496548
timeout = self.timeout
549+
550+
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
551+
request_data, request_content = _prepare_request_data_and_content(data, content)
552+
497553
req = self.client.build_request(
498-
"DELETE", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore
554+
"DELETE", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore
499555
)
500556
response = await self.client.send(req, stream=stream)
501557
response.raise_for_status()
@@ -543,8 +599,11 @@ async def single_connection_post_request(
543599
544600
Used for retrying connection client errors.
545601
"""
602+
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
603+
request_data, request_content = _prepare_request_data_and_content(data, content)
604+
546605
req = client.build_request(
547-
"POST", url, data=data, json=json, params=params, headers=headers, content=content # type: ignore
606+
"POST", url, data=request_data, json=json, params=params, headers=headers, content=request_content # type: ignore
548607
)
549608
response = await client.send(req, stream=stream)
550609
response.raise_for_status()
@@ -798,21 +857,24 @@ def post(
798857
logging_obj: Optional[LiteLLMLoggingObject] = None,
799858
):
800859
try:
860+
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
861+
request_data, request_content = _prepare_request_data_and_content(data, content)
862+
801863
if timeout is not None:
802864
req = self.client.build_request(
803865
"POST",
804866
url,
805-
data=data, # type: ignore
867+
data=request_data, # type: ignore
806868
json=json,
807869
params=params,
808870
headers=headers,
809871
timeout=timeout,
810872
files=files,
811-
content=content, # type: ignore
873+
content=request_content, # type: ignore
812874
)
813875
else:
814876
req = self.client.build_request(
815-
"POST", url, data=data, json=json, params=params, headers=headers, files=files, content=content # type: ignore
877+
"POST", url, data=request_data, json=json, params=params, headers=headers, files=files, content=request_content # type: ignore
816878
)
817879
response = self.client.send(req, stream=stream)
818880
response.raise_for_status()
@@ -840,21 +902,25 @@ def post(
840902
def patch(
841903
self,
842904
url: str,
843-
data: Optional[Union[dict, str]] = None,
905+
data: Optional[Union[dict, str, bytes]] = None,
844906
json: Optional[Union[dict, str]] = None,
845907
params: Optional[dict] = None,
846908
headers: Optional[dict] = None,
847909
stream: bool = False,
848910
timeout: Optional[Union[float, httpx.Timeout]] = None,
911+
content: Any = None,
849912
):
850913
try:
914+
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
915+
request_data, request_content = _prepare_request_data_and_content(data, content)
916+
851917
if timeout is not None:
852918
req = self.client.build_request(
853-
"PATCH", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore
919+
"PATCH", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore
854920
)
855921
else:
856922
req = self.client.build_request(
857-
"PATCH", url, data=data, json=json, params=params, headers=headers # type: ignore
923+
"PATCH", url, data=request_data, json=json, params=params, headers=headers, content=request_content # type: ignore
858924
)
859925
response = self.client.send(req, stream=stream)
860926
response.raise_for_status()
@@ -883,21 +949,25 @@ def patch(
883949
def put(
884950
self,
885951
url: str,
886-
data: Optional[Union[dict, str]] = None,
952+
data: Optional[Union[dict, str, bytes]] = None,
887953
json: Optional[Union[dict, str]] = None,
888954
params: Optional[dict] = None,
889955
headers: Optional[dict] = None,
890956
stream: bool = False,
891957
timeout: Optional[Union[float, httpx.Timeout]] = None,
958+
content: Any = None,
892959
):
893960
try:
961+
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
962+
request_data, request_content = _prepare_request_data_and_content(data, content)
963+
894964
if timeout is not None:
895965
req = self.client.build_request(
896-
"PUT", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore
966+
"PUT", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore
897967
)
898968
else:
899969
req = self.client.build_request(
900-
"PUT", url, data=data, json=json, params=params, headers=headers # type: ignore
970+
"PUT", url, data=request_data, json=json, params=params, headers=headers, content=request_content # type: ignore
901971
)
902972
response = self.client.send(req, stream=stream)
903973
return response
@@ -913,21 +983,25 @@ def put(
913983
def delete(
914984
self,
915985
url: str,
916-
data: Optional[Union[dict, str]] = None, # type: ignore
986+
data: Optional[Union[dict, str, bytes]] = None, # type: ignore
917987
json: Optional[dict] = None,
918988
params: Optional[dict] = None,
919989
headers: Optional[dict] = None,
920990
timeout: Optional[Union[float, httpx.Timeout]] = None,
921991
stream: bool = False,
992+
content: Any = None,
922993
):
923994
try:
995+
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
996+
request_data, request_content = _prepare_request_data_and_content(data, content)
997+
924998
if timeout is not None:
925999
req = self.client.build_request(
926-
"DELETE", url, data=data, json=json, params=params, headers=headers, timeout=timeout # type: ignore
1000+
"DELETE", url, data=request_data, json=json, params=params, headers=headers, timeout=timeout, content=request_content # type: ignore
9271001
)
9281002
else:
9291003
req = self.client.build_request(
930-
"DELETE", url, data=data, json=json, params=params, headers=headers # type: ignore
1004+
"DELETE", url, data=request_data, json=json, params=params, headers=headers, content=request_content # type: ignore
9311005
)
9321006
response = self.client.send(req, stream=stream)
9331007
response.raise_for_status()

0 commit comments

Comments
 (0)