Skip to content

Commit 1b098e7

Browse files
authored
fix: trim whitespace from API keys and host config (#525)
1 parent 8d82855 commit 1b098e7

5 files changed

Lines changed: 73 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/posthog: patch
3+
---
4+
5+
Trim surrounding whitespace from API keys and host config before using them.

posthog/client.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
)
4646
from posthog.poller import Poller
4747
from posthog.request import (
48-
DEFAULT_HOST,
4948
APIError,
5049
QuotaLimitError,
5150
RequestsConnectionError,
@@ -54,6 +53,7 @@
5453
determine_server_host,
5554
flags,
5655
get,
56+
normalize_host,
5757
remote_config,
5858
)
5959
from posthog.types import (
@@ -221,14 +221,14 @@ def __init__(
221221
self.queue = queue.Queue(max_queue_size)
222222

223223
# api_key: This should be the Team API Key (token), public
224-
self.api_key = project_api_key
224+
self.api_key = project_api_key.strip()
225225

226226
self.on_error = on_error
227227
self.debug = debug
228228
self.send = send
229229
self.sync_mode = sync_mode
230230
# Used for session replay URL generation - we don't want the server host here.
231-
self.raw_host = host or DEFAULT_HOST
231+
self.raw_host = normalize_host(host)
232232
self.host = determine_server_host(host)
233233
self.gzip = gzip
234234
self.timeout = timeout
@@ -278,7 +278,11 @@ def __init__(
278278
self.project_root = project_root
279279

280280
# personal_api_key: This should be a generated Personal API Key, private
281-
self.personal_api_key = personal_api_key
281+
self.personal_api_key = (
282+
personal_api_key.strip()
283+
if isinstance(personal_api_key, str)
284+
else personal_api_key
285+
) or None
282286
if debug:
283287
# Ensures that debug level messages are logged when debug mode is on.
284288
# Otherwise, defaults to WARNING level. See https://docs.python.org/3/howto/logging.html#what-happens-if-no-configuration-is-provided
@@ -287,6 +291,11 @@ def __init__(
287291
else:
288292
self.log.setLevel(logging.WARNING)
289293

294+
if not self.api_key:
295+
self.log.error(
296+
"api_key is empty after trimming whitespace; check your project API key"
297+
)
298+
290299
self._set_before_send(before_send)
291300

292301
if self.enable_exception_autocapture:
@@ -1288,12 +1297,19 @@ def _load_feature_flags(self):
12881297

12891298
def _fetch_feature_flags_from_api(self):
12901299
"""Fetch feature flags from the PostHog API."""
1300+
personal_api_key = self.personal_api_key
1301+
if personal_api_key is None:
1302+
self.log.warning(
1303+
"[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags."
1304+
)
1305+
return
1306+
12911307
try:
12921308
# Store old flags to detect changes
12931309
old_flags_by_key: dict[str, dict] = self.feature_flags_by_key or {}
12941310

12951311
response = get(
1296-
self.personal_api_key,
1312+
personal_api_key,
12971313
f"/flags/definitions?token={self.api_key}&send_cohorts",
12981314
self.host,
12991315
timeout=10,

posthog/request.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,17 @@ def disable_connection_reuse() -> None:
165165
USER_AGENT = "posthog-python/" + VERSION
166166

167167

168+
def normalize_host(host: Optional[str]) -> str:
169+
"""Normalize a configured host, defaulting blank values to DEFAULT_HOST."""
170+
normalized_host = (host or "").strip()
171+
if not normalized_host:
172+
return DEFAULT_HOST
173+
return normalized_host
174+
175+
168176
def determine_server_host(host: Optional[str]) -> str:
169177
"""Determines the server host to use."""
170-
host_or_default = host or DEFAULT_HOST
178+
host_or_default = normalize_host(host)
171179
trimmed_host = remove_trailing_slash(host_or_default)
172180
if trimmed_host in ("https://app.posthog.com", "https://us.posthog.com"):
173181
return US_INGESTION_ENDPOINT
@@ -190,7 +198,8 @@ def post(
190198
log = logging.getLogger("posthog")
191199
body = kwargs
192200
body["sentAt"] = datetime.now(tz=tzutc()).isoformat()
193-
url = remove_trailing_slash(host or DEFAULT_HOST) + path
201+
trimmed_host = remove_trailing_slash(normalize_host(host))
202+
url = trimmed_host + path
194203
body["api_key"] = api_key
195204
data = json.dumps(body, cls=DatetimeSerializer)
196205
log.debug("making request: %s to url: %s", data, url)
@@ -330,7 +339,8 @@ def get(
330339
- not_modified=False and data=response if server returns 200
331340
"""
332341
log = logging.getLogger("posthog")
333-
full_url = remove_trailing_slash(host or DEFAULT_HOST) + url
342+
trimmed_host = remove_trailing_slash(normalize_host(host))
343+
full_url = trimmed_host + url
334344
headers = {"Authorization": "Bearer %s" % api_key, "User-Agent": USER_AGENT}
335345

336346
if etag:

posthog/test/test_client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,38 @@ def setUp(self):
4141
def test_requires_api_key(self):
4242
self.assertRaises(TypeError, Client)
4343

44+
@parameterized.expand(
45+
[
46+
("valid_key", " \nphc_validkey\t ", "phc_validkey", False),
47+
("whitespace_only", " \n\t ", "", True),
48+
]
49+
)
50+
def test_trims_api_key_whitespace(
51+
self, _, raw_api_key, expected_api_key, expect_error_log
52+
):
53+
with mock.patch.object(Client.log, "error") as mock_error:
54+
client = Client(raw_api_key, send=False)
55+
56+
self.assertEqual(client.api_key, expected_api_key)
57+
if expect_error_log:
58+
mock_error.assert_called_once_with(
59+
"api_key is empty after trimming whitespace; check your project API key"
60+
)
61+
else:
62+
mock_error.assert_not_called()
63+
64+
def test_trims_host_and_personal_api_key_whitespace(self):
65+
client = Client(
66+
FAKE_TEST_API_KEY,
67+
host=" \nhttps://eu.posthog.com/\t ",
68+
personal_api_key=" \n\t ",
69+
send=False,
70+
)
71+
72+
self.assertEqual(client.raw_host, "https://eu.posthog.com/")
73+
self.assertEqual(client.host, "https://eu.i.posthog.com")
74+
self.assertIsNone(client.personal_api_key)
75+
4476
def test_empty_flush(self):
4577
self.client.flush()
4678

posthog/test/test_request.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ def test_get_removes_trailing_slash_from_host(self, mock_get):
348348
("https://app.posthog.com/", "https://us.i.posthog.com"),
349349
("https://eu.posthog.com/", "https://eu.i.posthog.com"),
350350
("https://us.posthog.com/", "https://us.i.posthog.com"),
351+
(" \nhttps://eu.posthog.com/\t ", "https://eu.i.posthog.com"),
352+
(" \n\t ", "https://us.i.posthog.com"),
351353
(None, "https://us.i.posthog.com"),
352354
],
353355
)

0 commit comments

Comments
 (0)