From f2c75bdbe03000be09b5ea87a68fee1c5753047e Mon Sep 17 00:00:00 2001 From: Chesars Date: Wed, 4 Mar 2026 22:20:59 -0300 Subject: [PATCH] fix(bfl): add timeout to polling requests, validate initial POST status code - Propagate timeout to each polling GET request to prevent indefinite hangs - Validate HTTP status code of initial POST before parsing JSON - Fix inline import and add 60s timeout to image URL download in _read_image_bytes --- .../black_forest_labs/image_edit/handler.py | 21 +++++++++++++++++++ .../image_edit/transformation.py | 3 +-- .../image_generation/handler.py | 20 ++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/litellm/llms/black_forest_labs/image_edit/handler.py b/litellm/llms/black_forest_labs/image_edit/handler.py index 9bf92c874fd..b621b113fb6 100644 --- a/litellm/llms/black_forest_labs/image_edit/handler.py +++ b/litellm/llms/black_forest_labs/image_edit/handler.py @@ -166,6 +166,7 @@ class BlackForestLabsImageEdit: initial_response=response, headers=headers, sync_client=sync_client, + timeout=timeout, ) # Transform response @@ -269,6 +270,7 @@ class BlackForestLabsImageEdit: initial_response=response, headers=headers, async_client=async_client, + timeout=timeout, ) # Transform response @@ -285,6 +287,7 @@ class BlackForestLabsImageEdit: sync_client: HTTPHandler, max_wait: float = DEFAULT_MAX_POLLING_TIME, interval: float = DEFAULT_POLLING_INTERVAL, + timeout: Optional[Union[float, httpx.Timeout]] = None, ) -> httpx.Response: """ Poll BFL API until result is ready (sync version). @@ -295,10 +298,18 @@ class BlackForestLabsImageEdit: sync_client: HTTP client max_wait: Maximum time to wait in seconds interval: Polling interval in seconds + timeout: Timeout for each individual polling request Returns: Final response with completed result """ + # Validate initial response status code + if initial_response.status_code >= 400: + raise BlackForestLabsError( + status_code=initial_response.status_code, + message=f"BFL initial request failed: {initial_response.text}", + ) + # Parse initial response to get polling URL try: response_data = initial_response.json() @@ -332,6 +343,7 @@ class BlackForestLabsImageEdit: response = sync_client.get( url=polling_url, headers=polling_headers, + timeout=timeout, ) if response.status_code != 200: @@ -367,10 +379,18 @@ class BlackForestLabsImageEdit: async_client: AsyncHTTPHandler, max_wait: float = DEFAULT_MAX_POLLING_TIME, interval: float = DEFAULT_POLLING_INTERVAL, + timeout: Optional[Union[float, httpx.Timeout]] = None, ) -> httpx.Response: """ Poll BFL API until result is ready (async version). """ + # Validate initial response status code + if initial_response.status_code >= 400: + raise BlackForestLabsError( + status_code=initial_response.status_code, + message=f"BFL initial request failed: {initial_response.text}", + ) + # Parse initial response to get polling URL try: response_data = initial_response.json() @@ -404,6 +424,7 @@ class BlackForestLabsImageEdit: response = await async_client.get( url=polling_url, headers=polling_headers, + timeout=timeout, ) if response.status_code != 200: diff --git a/litellm/llms/black_forest_labs/image_edit/transformation.py b/litellm/llms/black_forest_labs/image_edit/transformation.py index ccb63bde941..228dada44bb 100644 --- a/litellm/llms/black_forest_labs/image_edit/transformation.py +++ b/litellm/llms/black_forest_labs/image_edit/transformation.py @@ -185,8 +185,7 @@ class BlackForestLabsImageEditConfig(BaseImageEditConfig): elif isinstance(image, str): if image.startswith(("http://", "https://")): # Download image from URL - import httpx as _httpx - response = _httpx.get(image) + response = httpx.get(image, timeout=60.0) return response.content else: # Assume it's a file path diff --git a/litellm/llms/black_forest_labs/image_generation/handler.py b/litellm/llms/black_forest_labs/image_generation/handler.py index 11c69a4b43a..38c223523ff 100644 --- a/litellm/llms/black_forest_labs/image_generation/handler.py +++ b/litellm/llms/black_forest_labs/image_generation/handler.py @@ -163,6 +163,7 @@ class BlackForestLabsImageGeneration: initial_response=response, headers=headers, sync_client=sync_client, + timeout=timeout, ) # Transform response @@ -265,6 +266,7 @@ class BlackForestLabsImageGeneration: initial_response=response, headers=headers, async_client=async_client, + timeout=timeout, ) # Transform response @@ -282,10 +284,18 @@ class BlackForestLabsImageGeneration: sync_client: HTTPHandler, max_wait: float = DEFAULT_MAX_POLLING_TIME, interval: float = DEFAULT_POLLING_INTERVAL, + timeout: Optional[Union[float, httpx.Timeout]] = None, ) -> httpx.Response: """ Poll BFL API until result is ready (sync version). """ + # Validate initial response status code + if initial_response.status_code >= 400: + raise BlackForestLabsError( + status_code=initial_response.status_code, + message=f"BFL initial request failed: {initial_response.text}", + ) + # Parse initial response to get polling URL try: response_data = initial_response.json() @@ -319,6 +329,7 @@ class BlackForestLabsImageGeneration: response = sync_client.get( url=polling_url, headers=polling_headers, + timeout=timeout, ) if response.status_code != 200: @@ -354,10 +365,18 @@ class BlackForestLabsImageGeneration: async_client: AsyncHTTPHandler, max_wait: float = DEFAULT_MAX_POLLING_TIME, interval: float = DEFAULT_POLLING_INTERVAL, + timeout: Optional[Union[float, httpx.Timeout]] = None, ) -> httpx.Response: """ Poll BFL API until result is ready (async version). """ + # Validate initial response status code + if initial_response.status_code >= 400: + raise BlackForestLabsError( + status_code=initial_response.status_code, + message=f"BFL initial request failed: {initial_response.text}", + ) + # Parse initial response to get polling URL try: response_data = initial_response.json() @@ -391,6 +410,7 @@ class BlackForestLabsImageGeneration: response = await async_client.get( url=polling_url, headers=polling_headers, + timeout=timeout, ) if response.status_code != 200: