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
6 changes: 6 additions & 0 deletions src/apify_client/_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ def _make_request(stop_retrying: Callable, attempt: int) -> httpx.Response:
if response.status_code < 500 and response.status_code != HTTPStatus.TOO_MANY_REQUESTS: # noqa: PLR2004
logger.debug('Status code is not retryable', extra={'status_code': response.status_code})
stop_retrying()

# Read the response in case it is a stream, so we can raise the error properly
response.read()
raise ApifyApiError(response, attempt)

return retry_with_exp_backoff(
Expand Down Expand Up @@ -304,6 +307,9 @@ async def _make_request(stop_retrying: Callable, attempt: int) -> httpx.Response
if response.status_code < 500 and response.status_code != HTTPStatus.TOO_MANY_REQUESTS: # noqa: PLR2004
logger.debug('Status code is not retryable', extra={'status_code': response.status_code})
stop_retrying()

# Read the response in case it is a stream, so we can raise the error properly
await response.aread()
raise ApifyApiError(response, attempt)

return await retry_with_exp_backoff_async(
Expand Down
66 changes: 64 additions & 2 deletions tests/unit/test_client_errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from collections.abc import Generator
from collections.abc import AsyncIterator, Generator, Iterator

import httpx
import pytest
Expand All @@ -15,8 +15,18 @@
'invalidItems': {'0': ["should have required property 'name'"], '1': ["should have required property 'name'"]}
}

RAW_ERROR = (
b'{\n'
b' "error": {\n'
b' "type": "insufficient-permissions",\n'
b' "message": "Insufficient permissions for the Actor run. Make sure you\''
b're passing a correct API token and that it has the required permissions."\n'
b' }\n'
b'}'
)

@pytest.fixture(autouse=True)

@pytest.fixture
def mocked_response() -> Generator[respx.MockRouter]:
response_content = json.dumps(
{'error': {'message': _EXPECTED_MESSAGE, 'type': _EXPECTED_TYPE, 'data': _EXPECTED_DATA}}
Expand All @@ -26,6 +36,7 @@ def mocked_response() -> Generator[respx.MockRouter]:
yield respx_mock


@pytest.mark.usefixtures('mocked_response')
def test_client_apify_api_error_with_data() -> None:
"""Test that client correctly throws ApifyApiError with error data from response."""
client = HTTPClient()
Expand All @@ -38,6 +49,7 @@ def test_client_apify_api_error_with_data() -> None:
assert e.value.data == _EXPECTED_DATA


@pytest.mark.usefixtures('mocked_response')
async def test_async_client_apify_api_error_with_data() -> None:
"""Test that async client correctly throws ApifyApiError with error data from response."""
client = HTTPClientAsync()
Expand All @@ -48,3 +60,53 @@ async def test_async_client_apify_api_error_with_data() -> None:
assert e.value.message == _EXPECTED_MESSAGE
assert e.value.type == _EXPECTED_TYPE
assert e.value.data == _EXPECTED_DATA


def test_client_apify_api_error_streamed() -> None:
"""Test that client correctly throws ApifyApiError when the response has stream."""

error = json.loads(RAW_ERROR.decode())

class ByteStream(httpx._types.SyncByteStream):
def __iter__(self) -> Iterator[bytes]:
yield RAW_ERROR

def close(self) -> None:
pass

stream_url = 'http://some-stream-url.com'

client = HTTPClient()

with respx.mock() as respx_mock:
respx_mock.get(url=stream_url).mock(return_value=httpx.Response(stream=ByteStream(), status_code=403))
with pytest.raises(ApifyApiError) as e:
client.call(method='GET', url=stream_url, stream=True, parse_response=False)

assert e.value.message == error['error']['message']
assert e.value.type == error['error']['type']


async def test_async_client_apify_api_error_streamed() -> None:
"""Test that async client correctly throws ApifyApiError when the response has stream."""

error = json.loads(RAW_ERROR.decode())

class AsyncByteStream(httpx._types.AsyncByteStream):
async def __aiter__(self) -> AsyncIterator[bytes]:
yield RAW_ERROR

async def aclose(self) -> None:
pass

stream_url = 'http://some-stream-url.com'

client = HTTPClientAsync()

with respx.mock() as respx_mock:
respx_mock.get(url=stream_url).mock(return_value=httpx.Response(stream=AsyncByteStream(), status_code=403))
with pytest.raises(ApifyApiError) as e:
await client.call(method='GET', url=stream_url, stream=True, parse_response=False)

assert e.value.message == error['error']['message']
assert e.value.type == error['error']['type']
Loading