Skip to content

Commit 70e328f

Browse files
committed
Add extra_headers argument to allow setting additional user headers
1 parent 25ff4e5 commit 70e328f

File tree

3 files changed

+130
-2
lines changed

3 files changed

+130
-2
lines changed

src/apify_client/_http_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(
3838
min_delay_between_retries_millis: int = 500,
3939
timeout_secs: int = 360,
4040
stats: Statistics | None = None,
41+
extra_headers: dict | None = None,
4142
) -> None:
4243
self.max_retries = max_retries
4344
self.min_delay_between_retries_millis = min_delay_between_retries_millis
@@ -59,8 +60,10 @@ def __init__(
5960
if token is not None:
6061
headers['Authorization'] = f'Bearer {token}'
6162

62-
self.impit_client = impit.Client(headers=headers, follow_redirects=True, timeout=timeout_secs)
63-
self.impit_async_client = impit.AsyncClient(headers=headers, follow_redirects=True, timeout=timeout_secs)
63+
init_headers = {**(extra_headers or {}), **headers}
64+
65+
self.impit_client = impit.Client(headers=init_headers, follow_redirects=True, timeout=timeout_secs)
66+
self.impit_async_client = impit.AsyncClient(headers=init_headers, follow_redirects=True, timeout=timeout_secs)
6467

6568
self.stats = stats or Statistics()
6669

src/apify_client/client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def __init__(
113113
max_retries: int | None = 8,
114114
min_delay_between_retries_millis: int | None = 500,
115115
timeout_secs: int | None = DEFAULT_TIMEOUT,
116+
extra_headers: dict | None = None,
116117
) -> None:
117118
"""Initialize a new instance.
118119
@@ -126,6 +127,7 @@ def __init__(
126127
min_delay_between_retries_millis: How long will the client wait between retrying requests
127128
(increases exponentially from this value).
128129
timeout_secs: The socket timeout of the HTTP requests sent to the Apify API.
130+
extra_headers: Additional headers to include in all requests.
129131
"""
130132
super().__init__(
131133
token,
@@ -143,6 +145,7 @@ def __init__(
143145
min_delay_between_retries_millis=self.min_delay_between_retries_millis,
144146
timeout_secs=self.timeout_secs,
145147
stats=self.stats,
148+
extra_headers=extra_headers,
146149
)
147150

148151
def actor(self, actor_id: str) -> ActorClient:
@@ -301,6 +304,7 @@ def __init__(
301304
max_retries: int | None = 8,
302305
min_delay_between_retries_millis: int | None = 500,
303306
timeout_secs: int | None = DEFAULT_TIMEOUT,
307+
extra_headers: dict | None = None,
304308
) -> None:
305309
"""Initialize a new instance.
306310
@@ -314,6 +318,7 @@ def __init__(
314318
min_delay_between_retries_millis: How long will the client wait between retrying requests
315319
(increases exponentially from this value).
316320
timeout_secs: The socket timeout of the HTTP requests sent to the Apify API.
321+
extra_headers: Additional headers to include in all requests.
317322
"""
318323
super().__init__(
319324
token,
@@ -331,6 +336,7 @@ def __init__(
331336
min_delay_between_retries_millis=self.min_delay_between_retries_millis,
332337
timeout_secs=self.timeout_secs,
333338
stats=self.stats,
339+
extra_headers=extra_headers,
334340
)
335341

336342
def actor(self, actor_id: str) -> ActorClientAsync:

tests/unit/test_client_headers.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import os
5+
import sys
6+
from importlib import metadata
7+
from typing import TYPE_CHECKING
8+
9+
from werkzeug import Request, Response
10+
11+
from apify_client._http_client import HTTPClient, HTTPClientAsync
12+
13+
if TYPE_CHECKING:
14+
from pytest_httpserver import HTTPServer
15+
16+
17+
def _header_handler(request: Request) -> Response:
18+
return Response(
19+
status=200,
20+
headers={},
21+
response=json.dumps({'received_headers': dict(request.headers)}),
22+
)
23+
24+
25+
def _get_user_agent() -> str:
26+
is_at_home = 'APIFY_IS_AT_HOME' in os.environ
27+
python_version = '.'.join([str(x) for x in sys.version_info[:3]])
28+
client_version = metadata.version('apify-client')
29+
return f'ApifyClient/{client_version} ({sys.platform}; Python/{python_version}); isAtHome/{is_at_home}'
30+
31+
32+
async def test_default_headers_async(httpserver: HTTPServer) -> None:
33+
"""Test that default headers are sent with each request."""
34+
35+
client = HTTPClientAsync(token='placeholder_token')
36+
httpserver.expect_request('/').respond_with_handler(_header_handler)
37+
api_url = httpserver.url_for('/').removesuffix('/')
38+
39+
response = await client.call(method='GET', url=f'{api_url}/')
40+
41+
request_headers = json.loads(response.text)['received_headers']
42+
43+
assert request_headers == {
44+
'User-Agent': _get_user_agent(),
45+
'Accept': 'application/json, */*',
46+
'Authorization': 'Bearer placeholder_token',
47+
'Accept-Encoding': 'gzip, br, zstd, deflate',
48+
'Host': f'{httpserver.host}:{httpserver.port}',
49+
}
50+
51+
52+
def test_default_headers_sync(httpserver: HTTPServer) -> None:
53+
"""Test that default headers are sent with each request."""
54+
55+
client = HTTPClient(token='placeholder_token')
56+
httpserver.expect_request('/').respond_with_handler(_header_handler)
57+
api_url = httpserver.url_for('/').removesuffix('/')
58+
59+
response = client.call(method='GET', url=f'{api_url}/')
60+
61+
request_headers = json.loads(response.text)['received_headers']
62+
63+
assert request_headers == {
64+
'User-Agent': _get_user_agent(),
65+
'Accept': 'application/json, */*',
66+
'Authorization': 'Bearer placeholder_token',
67+
'Accept-Encoding': 'gzip, br, zstd, deflate',
68+
'Host': f'{httpserver.host}:{httpserver.port}',
69+
}
70+
71+
72+
async def test_extra_headers_async(httpserver: HTTPServer) -> None:
73+
"""Test that extra headers are sent with each request."""
74+
75+
extra_headers = {
76+
'Test-Header': 'blah',
77+
'User-Agent': 'CustomUserAgent/1.0', # Do not override Apify User-Agent
78+
}
79+
client = HTTPClientAsync(token='placeholder_token', extra_headers=extra_headers)
80+
httpserver.expect_request('/').respond_with_handler(_header_handler)
81+
api_url = httpserver.url_for('/').removesuffix('/')
82+
83+
response = await client.call(method='GET', url=f'{api_url}/')
84+
85+
request_headers = json.loads(response.text)['received_headers']
86+
87+
assert request_headers == {
88+
'Test-Header': 'blah',
89+
'User-Agent': _get_user_agent(), # Do not override Apify User-Agent
90+
'Accept': 'application/json, */*',
91+
'Authorization': 'Bearer placeholder_token',
92+
'Accept-Encoding': 'gzip, br, zstd, deflate',
93+
'Host': f'{httpserver.host}:{httpserver.port}',
94+
}
95+
96+
97+
def test_extra_headers_sync(httpserver: HTTPServer) -> None:
98+
"""Test that extra headers are sent with each request."""
99+
100+
extra_headers = {
101+
'Test-Header': 'blah',
102+
'User-Agent': 'CustomUserAgent/1.0', # Do not override Apify User-Agent
103+
}
104+
client = HTTPClient(token='placeholder_token', extra_headers=extra_headers)
105+
httpserver.expect_request('/').respond_with_handler(_header_handler)
106+
api_url = httpserver.url_for('/').removesuffix('/')
107+
108+
response = client.call(method='GET', url=f'{api_url}/')
109+
110+
request_headers = json.loads(response.text)['received_headers']
111+
112+
assert request_headers == {
113+
'Test-Header': 'blah',
114+
'User-Agent': _get_user_agent(), # Do not override Apify User-Agent
115+
'Accept': 'application/json, */*',
116+
'Authorization': 'Bearer placeholder_token',
117+
'Accept-Encoding': 'gzip, br, zstd, deflate',
118+
'Host': f'{httpserver.host}:{httpserver.port}',
119+
}

0 commit comments

Comments
 (0)