Skip to content

Commit f9fc39c

Browse files
vdusekclaude
andauthored
docs: add custom HTTP clients concept page and HTTPX guide (#658)
## Summary - Basically documenting #416. ## Description - Add concept page documenting the pluggable HTTP client architecture (default client config, abstract base classes, `HttpResponse` protocol, `call` method parameters) - Add guide page with step-by-step HTTPX implementation example (sync + async) - Add `ApiLink` component for linking to API reference from docs Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a288545 commit f9fc39c

11 files changed

+432
-1
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
id: custom-http-clients
3+
title: Custom HTTP clients
4+
---
5+
6+
import Tabs from '@theme/Tabs';
7+
import TabItem from '@theme/TabItem';
8+
import CodeBlock from '@theme/CodeBlock';
9+
import ApiLink from '@site/src/components/ApiLink';
10+
11+
import DefaultHttpClientAsyncExample from '!!raw-loader!./code/10_default_http_client_async.py';
12+
import DefaultHttpClientSyncExample from '!!raw-loader!./code/10_default_http_client_sync.py';
13+
14+
import ArchitectureImportsExample from '!!raw-loader!./code/10_architecture_imports.py';
15+
16+
import PluggingInAsyncExample from '!!raw-loader!./code/10_plugging_in_async.py';
17+
import PluggingInSyncExample from '!!raw-loader!./code/10_plugging_in_sync.py';
18+
19+
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.
20+
21+
## Default HTTP client
22+
23+
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:
24+
25+
- Automatic retries with exponential backoff for network errors, HTTP 429, and HTTP 5xx responses.
26+
- Configurable timeouts.
27+
- Preparing request data and headers according to the API requirements, including authentication.
28+
- Collecting requests statistics for monitoring and debugging.
29+
30+
You can configure the default client through the <ApiLink to="class/ApifyClient">`ApifyClient`</ApiLink> or <ApiLink to="class/ApifyClientAsync">`ApifyClientAsync`</ApiLink> constructor:
31+
32+
<Tabs>
33+
<TabItem value="AsyncExample" label="Async client" default>
34+
<CodeBlock className="language-python">
35+
{DefaultHttpClientAsyncExample}
36+
</CodeBlock>
37+
</TabItem>
38+
<TabItem value="SyncExample" label="Sync client">
39+
<CodeBlock className="language-python">
40+
{DefaultHttpClientSyncExample}
41+
</CodeBlock>
42+
</TabItem>
43+
</Tabs>
44+
45+
## Architecture
46+
47+
The HTTP client system is built on two key abstractions:
48+
49+
- <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.
50+
- <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.
51+
52+
To plug in your custom implementation, use the <ApiLink to="class/ApifyClient#with_custom_http_client">`ApifyClient.with_custom_http_client`</ApiLink> class method.
53+
54+
All of these are available as top-level imports from the `apify_client` package:
55+
56+
<CodeBlock className="language-python">
57+
{ArchitectureImportsExample}
58+
</CodeBlock>
59+
60+
### The call method
61+
62+
The `call` method receives all the information needed to make an HTTP request:
63+
64+
- `method` - HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.).
65+
- `url` - Full URL to make the request to.
66+
- `headers` - Additional headers to include.
67+
- `params` - Query parameters to append to the URL.
68+
- `data` - Raw request body (mutually exclusive with `json`).
69+
- `json` - JSON-serializable request body (mutually exclusive with `data`).
70+
- `stream` - Whether to stream the response body.
71+
- `timeout` - Timeout for the request as a `timedelta`.
72+
73+
It must return an object satisfying the <ApiLink to="class/HttpResponse">`HttpResponse`</ApiLink> protocol.
74+
75+
### The HTTP response protocol
76+
77+
<ApiLink to="class/HttpResponse">`HttpResponse`</ApiLink> is not a concrete class. Any object with the following attributes and methods will work:
78+
79+
| Property / Method | Description |
80+
|---|---|
81+
| `status_code: int` | HTTP status code |
82+
| `text: str` | Response body as text |
83+
| `content: bytes` | Raw response body |
84+
| `headers: Mapping[str, str]` | Response headers |
85+
| `json() -> Any` | Parse body as JSON |
86+
| `read() -> bytes` | Read entire response body |
87+
| `aread() -> bytes` | Read entire response body (async) |
88+
| `close() -> None` | Close the response |
89+
| `aclose() -> None` | Close the response (async) |
90+
| `iter_bytes() -> Iterator[bytes]` | Iterate body in chunks |
91+
| `aiter_bytes() -> AsyncIterator[bytes]` | Iterate body in chunks (async) |
92+
93+
:::note
94+
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.
95+
:::
96+
97+
### Plugging it in
98+
99+
Use the <ApiLink to="class/ApifyClient#with_custom_http_client">`ApifyClient.with_custom_http_client`</ApiLink> (or <ApiLink to="class/ApifyClientAsync#with_custom_http_client">`ApifyClientAsync.with_custom_http_client`</ApiLink>) class method to create a client with your custom implementation:
100+
101+
<Tabs>
102+
<TabItem value="AsyncExample" label="Async client" default>
103+
<CodeBlock className="language-python">
104+
{PluggingInAsyncExample}
105+
</CodeBlock>
106+
</TabItem>
107+
<TabItem value="SyncExample" label="Sync client">
108+
<CodeBlock className="language-python">
109+
{PluggingInSyncExample}
110+
</CodeBlock>
111+
</TabItem>
112+
</Tabs>
113+
114+
After that, all API calls made through the client will go through your custom HTTP client.
115+
116+
:::warning
117+
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.
118+
:::
119+
120+
## Use cases
121+
122+
Custom HTTP clients might be useful when you need to:
123+
124+
- **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/).
125+
- **Route through a proxy** - Add proxy support or request routing.
126+
- **Implement custom retry logic** - Use different backoff strategies or retry conditions.
127+
- **Log requests and responses** - Track API calls for debugging or auditing.
128+
- **Modify requests** - Add custom fields, modify the body, or change headers.
129+
- **Collect custom metrics** - Measure request latency, track error rates, or count API calls.
130+
131+
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.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from apify_client import (
2+
HttpClient,
3+
HttpClientAsync,
4+
HttpResponse,
5+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from datetime import timedelta
2+
3+
from apify_client import ApifyClientAsync
4+
5+
TOKEN = 'MY-APIFY-TOKEN'
6+
7+
8+
async def main() -> None:
9+
client = ApifyClientAsync(
10+
token=TOKEN,
11+
max_retries=4,
12+
min_delay_between_retries=timedelta(milliseconds=500),
13+
timeout=timedelta(seconds=360),
14+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from datetime import timedelta
2+
3+
from apify_client import ApifyClient
4+
5+
TOKEN = 'MY-APIFY-TOKEN'
6+
7+
8+
def main() -> None:
9+
client = ApifyClient(
10+
token=TOKEN,
11+
max_retries=4,
12+
min_delay_between_retries=timedelta(milliseconds=500),
13+
timeout=timedelta(seconds=360),
14+
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from datetime import timedelta
2+
from typing import Any
3+
4+
from apify_client import ApifyClientAsync, HttpClientAsync, HttpResponse
5+
6+
TOKEN = 'MY-APIFY-TOKEN'
7+
8+
9+
class MyHttpClientAsync(HttpClientAsync):
10+
"""Custom async HTTP client."""
11+
12+
async def call(
13+
self,
14+
*,
15+
method: str,
16+
url: str,
17+
headers: dict[str, str] | None = None,
18+
params: dict[str, Any] | None = None,
19+
data: str | bytes | bytearray | None = None,
20+
json: Any = None,
21+
stream: bool | None = None,
22+
timeout: timedelta | None = None,
23+
) -> HttpResponse: ...
24+
25+
26+
async def main() -> None:
27+
client = ApifyClientAsync.with_custom_http_client(
28+
token=TOKEN,
29+
http_client=MyHttpClientAsync(),
30+
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from datetime import timedelta
2+
from typing import Any
3+
4+
from apify_client import ApifyClient, HttpClient, HttpResponse
5+
6+
TOKEN = 'MY-APIFY-TOKEN'
7+
8+
9+
class MyHttpClient(HttpClient):
10+
"""Custom sync HTTP client."""
11+
12+
def call(
13+
self,
14+
*,
15+
method: str,
16+
url: str,
17+
headers: dict[str, str] | None = None,
18+
params: dict[str, Any] | None = None,
19+
data: str | bytes | bytearray | None = None,
20+
json: Any = None,
21+
stream: bool | None = None,
22+
timeout: timedelta | None = None,
23+
) -> HttpResponse: ...
24+
25+
26+
def main() -> None:
27+
client = ApifyClient.with_custom_http_client(
28+
token=TOKEN,
29+
http_client=MyHttpClient(),
30+
)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
id: custom-http-client-httpx
3+
title: Using HTTPX as the HTTP client
4+
---
5+
6+
import ApiLink from '@site/src/components/ApiLink';
7+
import Tabs from '@theme/Tabs';
8+
import TabItem from '@theme/TabItem';
9+
import CodeBlock from '@theme/CodeBlock';
10+
11+
import CustomHttpClientAsyncExample from '!!raw-loader!./code/05_custom_http_client_async.py';
12+
import CustomHttpClientSyncExample from '!!raw-loader!./code/05_custom_http_client_sync.py';
13+
14+
This guide shows how to replace the default <ApiLink to="class/ImpitHttpClient">`ImpitHttpClient`</ApiLink> and <ApiLink to="class/ImpitHttpClientAsync">`ImpitHttpClientAsync`</ApiLink> with one based on [HTTPX](https://www.python-httpx.org/). The same approach works for any HTTP library — see [Custom HTTP clients](/api/client/python/docs/concepts/custom-http-clients) for the underlying architecture.
15+
16+
## Why HTTPX?
17+
18+
You might want to use [HTTPX](https://www.python-httpx.org/) instead of the default [Impit](https://github.com/apify/impit)-based client for reasons like:
19+
20+
- You already use HTTPX in your project and want a single HTTP stack.
21+
- You need HTTPX-specific features.
22+
- You want fine-grained control over connection pooling or proxy routing.
23+
24+
## Implementation
25+
26+
The implementation involves two steps:
27+
28+
1. **Extend <ApiLink to="class/HttpClient">`HttpClient`</ApiLink> (sync) or <ApiLink to="class/HttpClientAsync">`HttpClientAsync`</ApiLink> (async)** and implement the `call` method that delegates to HTTPX.
29+
2. **Pass it to <ApiLink to="class/ApifyClient#with_custom_http_client">`ApifyClient.with_custom_http_client`</ApiLink>** to create a client that uses your implementation.
30+
31+
The `call` method receives parameters like `method`, `url`, `headers`, `params`, `data`, `json`, `stream`, and `timeout`. Map them to the corresponding HTTPX arguments — most map directly, except `data` which becomes HTTPX's `content` parameter and `timeout` which needs conversion from `timedelta` to seconds.
32+
33+
A convenient property of HTTPX is that its `httpx.Response` object already satisfies the <ApiLink to="class/HttpResponse">`HttpResponse`</ApiLink> protocol, so you can return it directly without wrapping.
34+
35+
<Tabs>
36+
<TabItem value="AsyncExample" label="Async client" default>
37+
<CodeBlock className="language-python">
38+
{CustomHttpClientAsyncExample}
39+
</CodeBlock>
40+
</TabItem>
41+
<TabItem value="SyncExample" label="Sync client">
42+
<CodeBlock className="language-python">
43+
{CustomHttpClientSyncExample}
44+
</CodeBlock>
45+
</TabItem>
46+
</Tabs>
47+
48+
:::warning
49+
When using a custom HTTP client, you are responsible for handling retries, timeouts, and error handling yourself. The built-in retry logic with exponential backoff is part of the default <ApiLink to="class/ImpitHttpClient">`ImpitHttpClient`</ApiLink> and is not applied to custom implementations.
50+
:::
51+
52+
## Going further
53+
54+
The example above is minimal on purpose. In a production setup, you might want to extend it with:
55+
56+
- **Retry logic** - Use [HTTPX's event hooks](https://www.python-httpx.org/advanced/event-hooks/) or utilize library like [tenacity](https://tenacity.readthedocs.io/) to retry failed requests.
57+
- **Custom headers** - You can add headers in the `call` method before delegating to HTTPX.
58+
- **Connection lifecycle** - Close the underlying `httpx.Client` when done by adding a `close()` method to your custom client.
59+
- **Proxy support** - You can pass `proxy=...` when creating the `httpx.Client`.
60+
- **Metrics collection** - Track request latency, error rates, or other metrics by adding instrumentation in the `call` method.
61+
- **Logging** - Log requests and responses for debugging or auditing purposes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from typing import TYPE_CHECKING, Any
5+
6+
import httpx
7+
8+
from apify_client import ApifyClientAsync, HttpClientAsync, HttpResponse
9+
10+
if TYPE_CHECKING:
11+
from datetime import timedelta
12+
13+
TOKEN = 'MY-APIFY-TOKEN'
14+
15+
16+
class HttpxClientAsync(HttpClientAsync):
17+
"""Custom async HTTP client using HTTPX library."""
18+
19+
def __init__(self) -> None:
20+
super().__init__()
21+
self._client = httpx.AsyncClient()
22+
23+
async def call(
24+
self,
25+
*,
26+
method: str,
27+
url: str,
28+
headers: dict[str, str] | None = None,
29+
params: dict[str, Any] | None = None,
30+
data: str | bytes | bytearray | None = None,
31+
json: Any = None,
32+
stream: bool | None = None,
33+
timeout: timedelta | None = None,
34+
) -> HttpResponse:
35+
timeout_secs = timeout.total_seconds() if timeout else 0
36+
37+
# httpx.Response satisfies the HttpResponse protocol,
38+
# so it can be returned directly.
39+
return await self._client.request(
40+
method=method,
41+
url=url,
42+
headers=headers,
43+
params=params,
44+
content=data,
45+
json=json,
46+
timeout=timeout_secs,
47+
)
48+
49+
50+
async def main() -> None:
51+
client = ApifyClientAsync.with_custom_http_client(
52+
token=TOKEN,
53+
http_client=HttpxClientAsync(),
54+
)
55+
56+
actor = await client.actor('apify/hello-world').get()
57+
print(actor)
58+
59+
60+
if __name__ == '__main__':
61+
asyncio.run(main())

0 commit comments

Comments
 (0)