Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 0.42.11

### Enhancements

### Features

### Fixes
* Retry on `httpx.RemoteProtocolError` (e.g. "Server disconnected without sending a response") when `retry_connection_errors=True`. Previously, mid-request server crashes were treated as permanent errors and not retried.

## 0.42.5

### Enhancements
Expand Down
129 changes: 129 additions & 0 deletions _test_unstructured_client/unit/test_retries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Tests for retry logic, specifically covering RemoteProtocolError retry behavior."""

import asyncio
from unittest.mock import MagicMock

import httpx
import pytest

from unstructured_client.utils.retries import (
BackoffStrategy,
PermanentError,
Retries,
RetryConfig,
retry,
retry_async,
)


def _make_retries(retry_connection_errors: bool) -> Retries:
return Retries(
config=RetryConfig(
strategy="backoff",
backoff=BackoffStrategy(
initial_interval=100,
max_interval=200,
exponent=1.5,
max_elapsed_time=5000,
),
retry_connection_errors=retry_connection_errors,
),
status_codes=[],
)


class TestRemoteProtocolErrorRetry:
"""Test that RemoteProtocolError (e.g. 'Server disconnected without sending a response')
is retried when retry_connection_errors=True."""

def test_remote_protocol_error_retried_when_enabled(self):
"""RemoteProtocolError should be retried and succeed on subsequent attempt."""
retries_config = _make_retries(retry_connection_errors=True)

mock_response = MagicMock(spec=httpx.Response)
mock_response.status_code = 200

call_count = 0

def func():
nonlocal call_count
call_count += 1
if call_count == 1:
raise httpx.RemoteProtocolError(
"Server disconnected without sending a response."
)
return mock_response

result = retry(func, retries_config)
assert result.status_code == 200
assert call_count == 2

def test_remote_protocol_error_not_retried_when_disabled(self):
"""RemoteProtocolError should raise PermanentError when retry_connection_errors=False."""
retries_config = _make_retries(retry_connection_errors=False)

def func():
raise httpx.RemoteProtocolError(
"Server disconnected without sending a response."
)

with pytest.raises(httpx.RemoteProtocolError):
retry(func, retries_config)

def test_connect_error_still_retried(self):
"""Existing ConnectError retry behavior should be preserved."""
retries_config = _make_retries(retry_connection_errors=True)

mock_response = MagicMock(spec=httpx.Response)
mock_response.status_code = 200

call_count = 0

def func():
nonlocal call_count
call_count += 1
if call_count == 1:
raise httpx.ConnectError("Connection refused")
return mock_response

result = retry(func, retries_config)
assert result.status_code == 200
assert call_count == 2


class TestRemoteProtocolErrorRetryAsync:
"""Async versions of the RemoteProtocolError retry tests."""

def test_remote_protocol_error_retried_async(self):
"""Async: RemoteProtocolError should be retried when retry_connection_errors=True."""
retries_config = _make_retries(retry_connection_errors=True)

mock_response = MagicMock(spec=httpx.Response)
mock_response.status_code = 200

call_count = 0

async def func():
nonlocal call_count
call_count += 1
if call_count == 1:
raise httpx.RemoteProtocolError(
"Server disconnected without sending a response."
)
return mock_response

result = asyncio.run(retry_async(func, retries_config))
assert result.status_code == 200
assert call_count == 2

def test_remote_protocol_error_not_retried_async_when_disabled(self):
"""Async: RemoteProtocolError should not be retried when retry_connection_errors=False."""
retries_config = _make_retries(retry_connection_errors=False)

async def func():
raise httpx.RemoteProtocolError(
"Server disconnected without sending a response."
)

with pytest.raises(httpx.RemoteProtocolError):
asyncio.run(retry_async(func, retries_config))
2 changes: 1 addition & 1 deletion gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ generation:
schemas:
allOfMergeStrategy: shallowMerge
python:
version: 0.42.10
version: 0.42.11
additionalDependencies:
dev:
deepdiff: '>=6.0'
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

[project]
name = "unstructured-client"
version = "0.42.10"
version = "0.42.11"
description = "Python Client SDK for Unstructured API"
authors = [{ name = "Unstructured" },]
readme = "README-PYPI.md"
Expand Down
4 changes: 2 additions & 2 deletions src/unstructured_client/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import importlib.metadata

__title__: str = "unstructured-client"
__version__: str = "0.42.10"
__version__: str = "0.42.11"
__openapi_doc_version__: str = "1.2.31"
__gen_version__: str = "2.680.0"
__user_agent__: str = "speakeasy-sdk/python 0.42.10 2.680.0 1.2.31 unstructured-client"
__user_agent__: str = "speakeasy-sdk/python 0.42.11 2.680.0 1.2.31 unstructured-client"

try:
if __package__ is not None:
Expand Down
10 changes: 10 additions & 0 deletions src/unstructured_client/utils/retries.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ def do_request() -> httpx.Response:
if retries.config.retry_connection_errors:
raise

raise PermanentError(exception) from exception
except httpx.RemoteProtocolError as exception:
if retries.config.retry_connection_errors:
raise

raise PermanentError(exception) from exception
except httpx.TimeoutException as exception:
if retries.config.retry_connection_errors:
Expand Down Expand Up @@ -137,6 +142,11 @@ async def do_request() -> httpx.Response:
if retries.config.retry_connection_errors:
raise

raise PermanentError(exception) from exception
except httpx.RemoteProtocolError as exception:
if retries.config.retry_connection_errors:
raise

raise PermanentError(exception) from exception
except httpx.TimeoutException as exception:
if retries.config.retry_connection_errors:
Expand Down
Loading