Skip to content

Commit 5f04389

Browse files
committed
feat: Add HTTPX-based HTTP client
1 parent 3caaa2b commit 5f04389

32 files changed

Lines changed: 2142 additions & 1205 deletions

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@
5757
uv add "apify-client[brotli]"
5858
```
5959

60+
[Impit](https://github.com/apify/impit) is the default HTTP client and is installed automatically. To use the
61+
built-in [HTTPX](https://www.python-httpx.org/) client instead, install its optional extra:
62+
63+
```bash
64+
pip install "apify-client[httpx]"
65+
# or
66+
uv add "apify-client[httpx]"
67+
```
68+
6069
- From [conda-forge](https://anaconda.org/conda-forge/apify-client), it can be installed with [conda](https://docs.conda.io/en/latest/):
6170

6271
```bash
@@ -124,7 +133,7 @@ For a guided walkthrough — authenticating, running an Actor, and reading its r
124133
- **Tiered timeouts** — short / medium / long tiers picked per endpoint, overridable per call ([Timeouts](https://docs.apify.com/api/client/python/docs/concepts/timeouts)).
125134
- **Pagination and streaming** — iterate datasets, key-value store keys, or live logs without manual paging or buffering ([Pagination](https://docs.apify.com/api/client/python/docs/concepts/pagination), [Streaming](https://docs.apify.com/api/client/python/docs/concepts/streaming-resources)).
126135
- **Convenience methods**`call()`, `wait_for_finish()`, nested resource access, and other shortcuts that hide platform quirks ([Convenience methods](https://docs.apify.com/api/client/python/docs/concepts/convenience-methods)).
127-
- **Pluggable HTTP layer**swap the default [Impit](https://github.com/apify/impit)-based HTTP client for `httpx`, `requests`, `aiohttp`, or any custom implementation ([Custom HTTP clients](https://docs.apify.com/api/client/python/docs/concepts/custom-http-clients)).
136+
- **Pluggable HTTP layer**use the default [Impit](https://github.com/apify/impit)-based client, opt in to the built-in [HTTPX](https://www.python-httpx.org/) client, or provide any custom implementation ([HTTP clients](https://docs.apify.com/api/client/python/docs/concepts/custom-http-clients)).
128137
- **Structured errors** — every API error surfaces as an [`ApifyApiError`](https://docs.apify.com/api/client/python/reference/class/ApifyApiError) with HTTP-specific subclasses for precise handling ([Error handling](https://docs.apify.com/api/client/python/docs/concepts/error-handling)).
129138
- **Debug logging** — opt-in structured logging on the `apify_client` logger captures request URLs, status codes, retry attempts, and more ([Logging](https://docs.apify.com/api/client/python/docs/concepts/logging)).
130139
@@ -192,7 +201,7 @@ The full documentation lives at **[docs.apify.com/api/client/python](https://doc
192201
| [Introduction](https://docs.apify.com/api/client/python/docs) | Overview, prerequisites, and a tour of the client. |
193202
| [Quick start](https://docs.apify.com/api/client/python/docs/quick-start) | Authenticate, run an Actor, and fetch its results step by step. |
194203
| [Concepts](https://docs.apify.com/api/client/python/docs/concepts/asyncio-support) | Asyncio, single vs. collection clients, nested clients, error handling, retries, logging, convenience methods, pagination, streaming, custom HTTP clients, timeouts. |
195-
| [Guides](https://docs.apify.com/api/client/python/docs/guides/passing-input-to-actor) | Pass input to an Actor, manage tasks for reusable input, retrieve Actor data, integrate with data libraries (e.g. Pandas), use HTTPX as the HTTP client. |
204+
| [Guides](https://docs.apify.com/api/client/python/docs/guides/passing-input-to-actor) | Pass input to an Actor, manage tasks for reusable input, retrieve Actor data, integrate with data libraries (e.g. Pandas), build a custom HTTP client. |
196205
| [Upgrading](https://docs.apify.com/api/client/python/docs/upgrading/upgrading-to-v3) | Migrating between major versions. |
197206
| [API reference](https://docs.apify.com/api/client/python/reference) | Generated reference for every class, method, and model. |
198207
| [Changelog](https://docs.apify.com/api/client/python/docs/changelog) | Release history and breaking changes. |

docs/01_introduction/index.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,25 @@ For better request-body compression, opt in to `brotli`, which compresses better
6363

6464
For details, see [HTTP compression](../02_concepts/13_http_compression.mdx).
6565

66+
The client uses [Impit](https://github.com/apify/impit) as its default HTTP transport. To use the built-in
67+
[HTTPX](https://www.python-httpx.org/) transport, install its optional dependency:
68+
69+
<Tabs>
70+
<TabItem value="PyPI" label="PyPI" default>
71+
```bash
72+
pip install "apify-client[httpx]"
73+
```
74+
</TabItem>
75+
<TabItem value="conda-forge" label="conda-forge">
76+
```bash
77+
conda install conda-forge::apify-client conda-forge::httpx
78+
```
79+
</TabItem>
80+
</Tabs>
81+
82+
See [HTTP clients](../02_concepts/10_custom_http_clients.mdx) for synchronous and asynchronous examples and details
83+
about the shared architecture.
84+
6685
## Quick example
6786

6887
The following example shows how to run an Actor and retrieve its results:

docs/02_concepts/10_custom_http_clients.mdx

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
id: custom-http-clients
3-
title: Custom HTTP clients
4-
description: Replace the default HTTP client with a custom implementation.
3+
title: HTTP clients
4+
description: Understand the built-in HTTP clients and the custom client interface.
55
---
66

77
import Tabs from '@theme/Tabs';
@@ -11,22 +11,26 @@ import ApiLink from '@theme/ApiLink';
1111

1212
import DefaultHttpClientAsyncExample from '!!raw-loader!./code/10_default_http_client_async.py';
1313
import DefaultHttpClientSyncExample from '!!raw-loader!./code/10_default_http_client_sync.py';
14+
import HttpxHttpClientAsyncExample from '!!raw-loader!./code/10_httpx_client_async.py';
15+
import HttpxHttpClientSyncExample from '!!raw-loader!./code/10_httpx_client_sync.py';
1416

1517
import ArchitectureImportsExample from '!!raw-loader!./code/10_architecture_imports.py';
1618

1719
import PluggingInAsyncExample from '!!raw-loader!./code/10_plugging_in_async.py';
1820
import PluggingInSyncExample from '!!raw-loader!./code/10_plugging_in_sync.py';
1921

20-
The Apify API client uses a pluggable HTTP client architecture. By default, it ships with an [Impit](https://github.com/apify/impit)-based HTTP client that handles retries, timeouts, passing headers, and more. You can replace it with your own implementation for use cases like custom logging, proxying, request modification, or integrating with a different HTTP library.
22+
The Apify API client uses a pluggable HTTP layer. It ships with an [Impit](https://github.com/apify/impit)-based default,
23+
offers [HTTPX](https://www.python-httpx.org/) as an optional built-in alternative, and accepts fully custom synchronous
24+
or asynchronous implementations.
2125

2226
## Default HTTP client
2327

2428
When you create an <ApiLink to="class/ApifyClient">`ApifyClient`</ApiLink> or <ApiLink to="class/ApifyClientAsync">`ApifyClientAsync`</ApiLink> instance, it automatically uses the built-in <ApiLink to="class/ImpitHttpClient">`ImpitHttpClient`</ApiLink> (or <ApiLink to="class/ImpitHttpClientAsync">`ImpitHttpClientAsync`</ApiLink>). This default client provides:
2529

2630
- Automatic retries with exponential backoff for network errors, HTTP 429, and HTTP 5xx responses.
2731
- Configurable timeouts.
28-
- Preparing request data and headers according to the API requirements, including authentication.
29-
- Collecting requests statistics for monitoring and debugging.
32+
- Request compression and preparation of API-compatible data, query parameters, and headers, including authentication.
33+
- API error handling, structured logging, and request statistics.
3034

3135
You can configure the default client through the <ApiLink to="class/ApifyClient">`ApifyClient`</ApiLink> or <ApiLink to="class/ApifyClientAsync">`ApifyClientAsync`</ApiLink> constructor:
3236

@@ -43,35 +47,94 @@ You can configure the default client through the <ApiLink to="class/ApifyClient"
4347
</TabItem>
4448
</Tabs>
4549

50+
## Built-in HTTPX client
51+
52+
The package also provides <ApiLink to="class/HttpxHttpClient">`HttpxHttpClient`</ApiLink> and
53+
<ApiLink to="class/HttpxHttpClientAsync">`HttpxHttpClientAsync`</ApiLink>. They use the same request preparation,
54+
compression, retry policy, timeout tiers and growth, error handling, logging, and statistics as the default Impit clients, with
55+
[HTTPX](https://www.python-httpx.org/) as the transport.
56+
57+
HTTPX is an optional dependency. Install `apify-client[httpx]`, then pass the appropriate client to
58+
<ApiLink to="class/ApifyClient#with_custom_http_client">`ApifyClient.with_custom_http_client`</ApiLink>. Impit remains
59+
the default even when the HTTPX extra is installed.
60+
61+
```bash
62+
pip install "apify-client[httpx]"
63+
# or
64+
uv add "apify-client[httpx]"
65+
```
66+
67+
<Tabs>
68+
<TabItem value="HttpxAsyncExample" label="Async client" default>
69+
<CodeBlock className="language-python">
70+
{HttpxHttpClientAsyncExample}
71+
</CodeBlock>
72+
</TabItem>
73+
<TabItem value="HttpxSyncExample" label="Sync client">
74+
<CodeBlock className="language-python">
75+
{HttpxHttpClientSyncExample}
76+
</CodeBlock>
77+
</TabItem>
78+
</Tabs>
79+
80+
Configure retries, timeout tiers, default headers, and compression on the HTTPX client instance. The token passed to
81+
`with_custom_http_client` is applied automatically unless the HTTP client already has an `Authorization` header.
82+
The examples use the clients as context managers so their connection pools are closed deterministically. If a context
83+
manager does not fit your application's lifecycle, call `close()` on `HttpxHttpClient` or `await aclose()` on
84+
`HttpxHttpClientAsync` during shutdown.
85+
86+
Timeout values are passed to the selected transport. Impit treats them as whole-request timeouts, while HTTPX applies
87+
its connect, read, write, and pool timeout semantics. In particular, an HTTPX read timeout limits inactivity between
88+
chunks rather than the total duration of a streamed response. The `no_timeout` option disables HTTPX's timeouts.
89+
4690
## Architecture
4791

48-
The HTTP client system is built on two key abstractions:
92+
Internally, the HTTP client hierarchy has three layers:
93+
94+
- A common internal base contains configuration and utilities shared by synchronous and asynchronous clients, including
95+
headers, request-body preparation, parameters, compression, and timeout tiers. It is not a public extension point.
96+
- <ApiLink to="class/HttpClient">`HttpClient`</ApiLink> and <ApiLink to="class/HttpClientAsync">`HttpClientAsync`</ApiLink>
97+
add the synchronous or asynchronous request pipeline, retry loop, transport hooks, and lifecycle interface.
98+
- The built-in Impit and HTTPX classes inherit directly from the corresponding sync or async class and adapt the
99+
underlying transport.
100+
101+
`HttpClient.is_timeout_error(exc)` and `HttpClientAsync.is_timeout_error(exc)` provide the public, transport-neutral way
102+
to determine whether an exception is a timeout. Their shared implementation recognizes Python's `TimeoutError`;
103+
transport adapters override it when their HTTP library defines additional timeout exception types. This lets
104+
higher-level features such as streamed logs classify timeouts without depending on Impit, HTTPX, or private
105+
implementation details.
106+
107+
Responses use one separate abstraction:
49108

50-
- <ApiLink to="class/HttpClient">`HttpClient`</ApiLink> / <ApiLink to="class/HttpClientAsync">`HttpClientAsync`</ApiLink> - Abstract base classes that define the interface. Extend one of these to create a custom HTTP client by implementing the `call` method.
51109
- <ApiLink to="class/HttpResponse">`HttpResponse`</ApiLink> - A [runtime-checkable protocol](https://docs.python.org/3/library/typing.html#typing.runtime_checkable) that defines the expected response shape. Any object with the required attributes and methods satisfies the protocol — no inheritance needed.
52110

53111
To plug in your custom implementation, use the <ApiLink to="class/ApifyClient#with_custom_http_client">`ApifyClient.with_custom_http_client`</ApiLink> class method.
54112

55-
All of these are available as top-level imports from the `apify_client` package:
113+
The built-in Impit and HTTPX classes are thin transport adapters over the request implementation in `HttpClient` and
114+
`HttpClientAsync`. Custom transport adapters implement the request, error-classification, and lifecycle hooks. They
115+
inherit request construction, retries, timeout growth, API error conversion, logging, and statistics from the base.
116+
117+
All of these are available from the `apify_client.http_clients` module:
56118

57119
<CodeBlock className="language-python">
58120
{ArchitectureImportsExample}
59121
</CodeBlock>
60122

61-
### The call method
123+
### The transport contract
62124

63-
The `call` method receives all the information needed to make an HTTP request:
125+
The public `call` method provides the shared request pipeline. A concrete transport implements these hooks:
64126

65-
- `method` - HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.).
66-
- `url` - Full URL to make the request to.
67-
- `headers` - Additional headers to include.
68-
- `params` - Query parameters to append to the URL.
69-
- `data` - Raw request body (mutually exclusive with `json`).
70-
- `json` - JSON-serializable request body (mutually exclusive with `data`).
71-
- `stream` - Whether to stream the response body.
72-
- `timeout` - Timeout for the request as a `timedelta`.
127+
- `send_request(...)` sends one prepared request and returns an `HttpResponse`. The inherited `call` needs it, so
128+
every transport adapter has to implement it.
129+
- `is_retryable_transport_error(exc)` classifies transport failures for the shared retry loop. The default classifies
130+
nothing as retryable, so a transport that skips it gives up on the first connection failure.
131+
- `is_timeout_error(exc)` identifies transport-specific timeout exceptions for higher-level client features. The
132+
default recognizes Python's `TimeoutError`.
133+
- `close()` or `aclose()` closes resources owned by the transport. The default does nothing, which is correct for a
134+
transport that owns no pool or session.
73135

74-
It must return an object satisfying the <ApiLink to="class/HttpResponse">`HttpResponse`</ApiLink> protocol.
136+
The `@override` decorators in the built-in Impit and HTTPX adapters make these implementations explicit and allow type
137+
checkers to catch misspelled or incompatible overrides.
75138

76139
### The HTTP response protocol
77140

@@ -93,6 +156,10 @@ It must return an object satisfying the <ApiLink to="class/HttpResponse">`HttpRe
93156

94157
:::note
95158
Many HTTP libraries, including our default [Impit](https://github.com/apify/impit) or for example [HTTPX](https://www.python-httpx.org/) already satisfy this protocol out of the box.
159+
160+
For a streamed response, consume the body inside its context manager with `iter_bytes()` / `aiter_bytes()`, or call
161+
`read()` / `aread()` before accessing `content`. Some transports, including HTTPX, intentionally reject `content` on
162+
an unread streamed response.
96163
:::
97164

98165
### Plugging it in
@@ -115,18 +182,23 @@ Use the <ApiLink to="class/ApifyClient#with_custom_http_client">`ApifyClient.wit
115182
After that, all API calls made through the client will go through your custom HTTP client.
116183

117184
:::warning
118-
When using a custom HTTP client, you are responsible for constructing the request, handling retries, timeouts, and errors yourself. The default retry logic is not applied.
185+
If you override `call` itself, your implementation becomes responsible for request preparation, retries, timeouts, API
186+
error conversion, logging, and statistics. Implementing the transport hooks and inheriting `call` keeps the shared
187+
behavior.
119188
:::
120189

121190
## Use cases
122191

123-
Custom HTTP clients might be useful when you need to:
192+
Custom HTTP clients might be useful when the built-in Impit and HTTPX clients do not cover your requirements, for
193+
example when you need to:
124194

125-
- **Use a different HTTP library** - Swap Impit for [httpx](https://www.python-httpx.org/), [requests](https://requests.readthedocs.io/), or [aiohttp](https://docs.aiohttp.org/).
195+
- **Use a different HTTP library** - Integrate [requests](https://requests.readthedocs.io/), [aiohttp](https://docs.aiohttp.org/), or another transport.
126196
- **Route through a proxy** - Add proxy support or request routing.
127197
- **Implement custom retry logic** - Use different backoff strategies or retry conditions.
128198
- **Log requests and responses** - Track API calls for debugging or auditing.
129199
- **Modify requests** - Add custom fields, modify the body, or change headers.
130200
- **Collect custom metrics** - Measure request latency, track error rates, or count API calls.
131201

132-
For a step-by-step walkthrough of building a custom HTTP client, see the [Using HTTPX as the HTTP client](/api/client/python/docs/guides/custom-http-client-httpx) guide.
202+
For complete synchronous and asynchronous implementations over a transport with a different response API, see
203+
[Build a custom HTTP client](../03_guides/05_custom_http_client.mdx). You can also refer to the
204+
<ApiLink to="class/HttpClient">`HttpClient` API reference</ApiLink> for the synchronous contract.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import asyncio
2+
3+
from apify_client import ApifyClientAsync
4+
from apify_client.http_clients import HttpxHttpClientAsync
5+
6+
7+
async def main() -> None:
8+
async with HttpxHttpClientAsync() as http_client:
9+
client = ApifyClientAsync.with_custom_http_client(
10+
token='MY-APIFY-TOKEN',
11+
http_client=http_client,
12+
)
13+
print(await client.actor('apify/hello-world').get())
14+
15+
16+
if __name__ == '__main__':
17+
asyncio.run(main())
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from apify_client import ApifyClient
2+
from apify_client.http_clients import HttpxHttpClient
3+
4+
5+
def main() -> None:
6+
with HttpxHttpClient() as http_client:
7+
client = ApifyClient.with_custom_http_client(
8+
token='MY-APIFY-TOKEN',
9+
http_client=http_client,
10+
)
11+
print(client.actor('apify/hello-world').get())

docs/02_concepts/code/10_plugging_in_async.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
1-
from typing import Any
1+
from typing_extensions import override
22

33
from apify_client import ApifyClientAsync
44
from apify_client.http_clients import HttpClientAsync, HttpResponse
5-
from apify_client.types import Timeout
65

76
TOKEN = 'MY-APIFY-TOKEN'
87

98

109
class MyHttpClientAsync(HttpClientAsync):
1110
"""Custom async HTTP client."""
1211

13-
async def call(
12+
@override
13+
async def send_request(
1414
self,
1515
*,
1616
method: str,
1717
url: str,
18-
headers: dict[str, str] | None = None,
19-
params: dict[str, Any] | None = None,
20-
data: str | bytes | bytearray | None = None,
21-
json: Any = None,
22-
stream: bool | None = None,
23-
timeout: Timeout = 'medium',
24-
) -> HttpResponse: ...
18+
headers: dict[str, str],
19+
content: bytes | None,
20+
timeout: float | None,
21+
stream: bool,
22+
) -> HttpResponse:
23+
"""Send one request through the custom transport."""
24+
raise NotImplementedError
25+
26+
@override
27+
def is_retryable_transport_error(self, exc: Exception) -> bool:
28+
# List the transport's transient failures here, e.g. its timeout
29+
# and connection errors. Returning False for everything opts out
30+
# of transport retries entirely.
31+
return isinstance(exc, TimeoutError)
2532

2633

2734
async def main() -> None:

docs/02_concepts/code/10_plugging_in_sync.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
1-
from typing import Any
1+
from typing_extensions import override
22

33
from apify_client import ApifyClient
44
from apify_client.http_clients import HttpClient, HttpResponse
5-
from apify_client.types import Timeout
65

76
TOKEN = 'MY-APIFY-TOKEN'
87

98

109
class MyHttpClient(HttpClient):
1110
"""Custom sync HTTP client."""
1211

13-
def call(
12+
@override
13+
def send_request(
1414
self,
1515
*,
1616
method: str,
1717
url: str,
18-
headers: dict[str, str] | None = None,
19-
params: dict[str, Any] | None = None,
20-
data: str | bytes | bytearray | None = None,
21-
json: Any = None,
22-
stream: bool | None = None,
23-
timeout: Timeout = 'medium',
24-
) -> HttpResponse: ...
18+
headers: dict[str, str],
19+
content: bytes | None,
20+
timeout: float | None,
21+
stream: bool,
22+
) -> HttpResponse:
23+
"""Send one request through the custom transport."""
24+
raise NotImplementedError
25+
26+
@override
27+
def is_retryable_transport_error(self, exc: Exception) -> bool:
28+
# List the transport's transient failures here, e.g. its timeout
29+
# and connection errors. Returning False for everything opts out
30+
# of transport retries entirely.
31+
return isinstance(exc, TimeoutError)
2532

2633

2734
def main() -> None:

0 commit comments

Comments
 (0)