Skip to content

Commit c279f5d

Browse files
committed
resolve
2 parents bfdf114 + 0c91ca5 commit c279f5d

File tree

5 files changed

+441
-7
lines changed

5 files changed

+441
-7
lines changed

src/apify_client/_http_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ def _make_request(stop_retrying: Callable, attempt: int) -> httpx.Response:
214214
if response.status_code < 500 and response.status_code != HTTPStatus.TOO_MANY_REQUESTS: # noqa: PLR2004
215215
logger.debug('Status code is not retryable', extra={'status_code': response.status_code})
216216
stop_retrying()
217+
218+
# Read the response in case it is a stream, so we can raise the error properly
219+
response.read()
217220
raise ApifyApiError(response, attempt)
218221

219222
return retry_with_exp_backoff(
@@ -304,6 +307,9 @@ async def _make_request(stop_retrying: Callable, attempt: int) -> httpx.Response
304307
if response.status_code < 500 and response.status_code != HTTPStatus.TOO_MANY_REQUESTS: # noqa: PLR2004
305308
logger.debug('Status code is not retryable', extra={'status_code': response.status_code})
306309
stop_retrying()
310+
311+
# Read the response in case it is a stream, so we can raise the error properly
312+
await response.aread()
307313
raise ApifyApiError(response, attempt)
308314

309315
return await retry_with_exp_backoff_async(

tests/unit/test_client_errors.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
from __future__ import annotations
22

3+
import json
34
from typing import TYPE_CHECKING
45

6+
import httpx
57
import pytest
8+
import respx
69

710
from apify_client._errors import ApifyApiError
811
from apify_client._http_client import HTTPClient, HTTPClientAsync
912

1013
if TYPE_CHECKING:
14+
from collections.abc import AsyncIterator, Iterator
15+
1116
from pytest_httpserver import HTTPServer
1217

1318
_TEST_PATH = '/errors'
@@ -17,6 +22,16 @@
1722
'invalidItems': {'0': ["should have required property 'name'"], '1': ["should have required property 'name'"]}
1823
}
1924

25+
RAW_ERROR = (
26+
b'{\n'
27+
b' "error": {\n'
28+
b' "type": "insufficient-permissions",\n'
29+
b' "message": "Insufficient permissions for the Actor run. Make sure you\''
30+
b're passing a correct API token and that it has the required permissions."\n'
31+
b' }\n'
32+
b'}'
33+
)
34+
2035

2136
@pytest.fixture
2237
def test_endpoint(httpserver: HTTPServer) -> str:
@@ -48,3 +63,53 @@ async def test_async_client_apify_api_error_with_data(test_endpoint: str) -> Non
4863
assert e.value.message == _EXPECTED_MESSAGE
4964
assert e.value.type == _EXPECTED_TYPE
5065
assert e.value.data == _EXPECTED_DATA
66+
67+
68+
def test_client_apify_api_error_streamed() -> None:
69+
"""Test that client correctly throws ApifyApiError when the response has stream."""
70+
71+
error = json.loads(RAW_ERROR.decode())
72+
73+
class ByteStream(httpx._types.SyncByteStream):
74+
def __iter__(self) -> Iterator[bytes]:
75+
yield RAW_ERROR
76+
77+
def close(self) -> None:
78+
pass
79+
80+
stream_url = 'http://some-stream-url.com'
81+
82+
client = HTTPClient()
83+
84+
with respx.mock() as respx_mock:
85+
respx_mock.get(url=stream_url).mock(return_value=httpx.Response(stream=ByteStream(), status_code=403))
86+
with pytest.raises(ApifyApiError) as e:
87+
client.call(method='GET', url=stream_url, stream=True, parse_response=False)
88+
89+
assert e.value.message == error['error']['message']
90+
assert e.value.type == error['error']['type']
91+
92+
93+
async def test_async_client_apify_api_error_streamed() -> None:
94+
"""Test that async client correctly throws ApifyApiError when the response has stream."""
95+
96+
error = json.loads(RAW_ERROR.decode())
97+
98+
class AsyncByteStream(httpx._types.AsyncByteStream):
99+
async def __aiter__(self) -> AsyncIterator[bytes]:
100+
yield RAW_ERROR
101+
102+
async def aclose(self) -> None:
103+
pass
104+
105+
stream_url = 'http://some-stream-url.com'
106+
107+
client = HTTPClientAsync()
108+
109+
with respx.mock() as respx_mock:
110+
respx_mock.get(url=stream_url).mock(return_value=httpx.Response(stream=AsyncByteStream(), status_code=403))
111+
with pytest.raises(ApifyApiError) as e:
112+
await client.call(method='GET', url=stream_url, stream=True, parse_response=False)
113+
114+
assert e.value.message == error['error']['message']
115+
assert e.value.type == error['error']['type']

website/docusaurus.config.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
/* eslint-disable global-require,import/no-extraneous-dependencies */
1+
const path = require('path');
2+
23
const { config } = require('@apify/docs-theme');
4+
35
const { externalLinkProcessor } = require('./tools/utils/externalLink');
46
const { groupSort } = require('./transformDocs.js');
5-
const path = require('path');
67

78
const { absoluteUrl } = config;
89

@@ -29,9 +30,8 @@ module.exports = {
2930
trailingSlash: false,
3031
organizationName: 'apify',
3132
projectName: 'apify-client-python',
32-
scripts: ['/js/custom.js'],
3333
favicon: 'img/favicon.ico',
34-
scripts: [...(config.scripts ?? [])],
34+
scripts: ['/js/custom.js', ...(config.scripts ?? [])],
3535
onBrokenLinks:
3636
/** @type {import('@docusaurus/types').ReportingSeverity} */ ('warn'),
3737
onBrokenMarkdownLinks:
@@ -106,6 +106,19 @@ module.exports = {
106106
},
107107
},
108108
],
109+
[
110+
'@signalwire/docusaurus-plugin-llms-txt',
111+
{
112+
content: {
113+
includeVersionedDocs: false,
114+
enableLlmsFullTxt: true,
115+
includeBlog: true,
116+
includeGeneratedIndex: false,
117+
includePages: true,
118+
relativePaths: false,
119+
},
120+
},
121+
],
109122
...config.plugins,
110123
],
111124
themeConfig: {

0 commit comments

Comments
 (0)