Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 11 additions & 1 deletion RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1190,4 +1190,14 @@ Based on:
### Generated
- [python v0.42.10] .
### Releases
- [PyPI v0.42.10] https://pypi.org/project/unstructured-client/0.42.10 - .
- [PyPI v0.42.10] https://pypi.org/project/unstructured-client/0.42.10 - .

## 2026-03-25 16:00:00
### Changes
Based on:
- OpenAPI Doc
- Speakeasy CLI 1.601.0 (2.680.0) https://github.com/speakeasy-api/speakeasy
### Generated
- [python v0.42.11] .
### Releases
- [PyPI v0.42.11] https://pypi.org/project/unstructured-client/0.42.11 - .
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