Skip to content

Commit 5891f34

Browse files
fix: Update SDK key validation to follow dotnet pattern
- Rename validate_sdk_key to is_valid_sdk_key_format - Use dotnet regex pattern ^[-a-zA-Z0-9._]+$ instead of HTTP header validation - Add maximum length validation of 8192 characters - Change from raising ValueError to silently not setting invalid keys - Update all tests to expect silent failure instead of exceptions - Update documentation to reflect new behavior Co-Authored-By: jbailey@launchdarkly.com <accounts@sidewaysgravity.com>
1 parent 6b0920b commit 5891f34

File tree

4 files changed

+80
-49
lines changed

4 files changed

+80
-49
lines changed

ldclient/config.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from ldclient.feature_store import InMemoryFeatureStore
1111
from ldclient.hook import Hook
12-
from ldclient.impl.util import log, validate_application_info, validate_sdk_key
12+
from ldclient.impl.util import log, validate_application_info, is_valid_sdk_key_format
1313
from ldclient.interfaces import (
1414
BigSegmentStore,
1515
DataSourceUpdateSink,
@@ -261,10 +261,10 @@ def __init__(
261261
:param omit_anonymous_contexts: Sets whether anonymous contexts should be omitted from index and identify events.
262262
:param payload_filter_key: The payload filter is used to selectively limited the flags and segments delivered in the data source payload.
263263
"""
264-
if sdk_key and not validate_sdk_key(sdk_key, log):
265-
raise ValueError("SDK key contains invalid characters")
266-
267-
self.__sdk_key = sdk_key
264+
if is_valid_sdk_key_format(sdk_key):
265+
self.__sdk_key = sdk_key
266+
else:
267+
self.__sdk_key = None
268268

269269
self.__base_uri = base_uri.rstrip('/')
270270
self.__events_uri = events_uri.rstrip('/')
@@ -305,6 +305,7 @@ def __init__(
305305

306306
def copy_with_new_sdk_key(self, new_sdk_key: str) -> 'Config':
307307
"""Returns a new ``Config`` instance that is the same as this one, except for having a different SDK key.
308+
The key will not be updated if the provided key contains invalid characters.
308309
309310
:param new_sdk_key: the new SDK key
310311
"""

ldclient/impl/util.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,21 @@ def validate_application_value(value: Any, name: str, logger: logging.Logger) ->
5353
return value
5454

5555

56-
def validate_sdk_key(sdk_key: str, logger: logging.Logger) -> bool:
56+
def is_valid_sdk_key_format(sdk_key: str) -> bool:
5757
"""
58-
Validate that an SDK key contains only characters that are valid for HTTP headers.
59-
Returns True if valid, False if invalid. Logs a generic error message for invalid keys.
58+
Validates that a string does not contain invalid characters and is not too long for our systems.
59+
Returns True if the SDK key format is valid, otherwise False.
6060
"""
61+
if sdk_key is None or sdk_key == '':
62+
return True
63+
6164
if not isinstance(sdk_key, str):
62-
logger.warning("SDK key must be a string")
6365
return False
6466

65-
if sdk_key == '':
66-
return True # Empty keys are handled separately in _validate()
67+
if len(sdk_key) > 8192:
68+
return False
6769

68-
if re.search(r"[^\x21-\x7E]", sdk_key):
69-
logger.warning("SDK key contains invalid characters")
70+
if not re.match(r'^[-a-zA-Z0-9._]+$', sdk_key):
7071
return False
7172

7273
return True

ldclient/testing/impl/test_util.py

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,53 @@
1-
import logging
2-
from unittest.mock import Mock
3-
from ldclient.impl.util import validate_sdk_key
1+
from ldclient.impl.util import is_valid_sdk_key_format
42

53

6-
def test_validate_sdk_key_valid():
4+
def test_is_valid_sdk_key_format_valid():
75
"""Test validation of valid SDK keys"""
8-
logger = Mock(spec=logging.Logger)
9-
106
valid_keys = [
117
"sdk-12345678-1234-1234-1234-123456789012",
128
"valid-sdk-key-123",
13-
"VALID_SDK_KEY_456"
9+
"VALID_SDK_KEY_456",
10+
"test.key_with.dots",
11+
"test-key-with-hyphens"
1412
]
1513

1614
for key in valid_keys:
17-
assert validate_sdk_key(key, logger) is True
18-
logger.warning.assert_not_called()
19-
logger.reset_mock()
15+
assert is_valid_sdk_key_format(key) is True
2016

2117

22-
def test_validate_sdk_key_invalid():
18+
def test_is_valid_sdk_key_format_invalid():
2319
"""Test validation of invalid SDK keys"""
24-
logger = Mock(spec=logging.Logger)
25-
2620
invalid_keys = [
2721
"sdk-key-with-\x00-null",
2822
"sdk-key-with-\n-newline",
29-
"sdk-key-with-\t-tab"
23+
"sdk-key-with-\t-tab",
24+
"sdk key with spaces",
25+
"sdk@key#with$special%chars",
26+
"sdk/key\\with/slashes"
3027
]
3128

3229
for key in invalid_keys:
33-
assert validate_sdk_key(key, logger) is False
34-
logger.warning.assert_called_with("SDK key contains invalid characters")
35-
logger.reset_mock()
30+
assert is_valid_sdk_key_format(key) is False
3631

3732

38-
def test_validate_sdk_key_non_string():
33+
def test_is_valid_sdk_key_format_non_string():
3934
"""Test validation of non-string SDK keys"""
40-
logger = Mock(spec=logging.Logger)
41-
42-
non_string_values = [123, None, object(), [], {}]
35+
non_string_values = [123, object(), [], {}]
4336

4437
for value in non_string_values:
45-
result = validate_sdk_key(value, logger)
46-
assert result is False
47-
logger.warning.assert_called_with("SDK key must be a string")
48-
logger.reset_mock()
38+
assert is_valid_sdk_key_format(value) is False
39+
40+
41+
def test_is_valid_sdk_key_format_empty_and_none():
42+
"""Test validation of empty and None SDK keys"""
43+
assert is_valid_sdk_key_format("") is True
44+
assert is_valid_sdk_key_format(None) is True
4945

5046

51-
def test_validate_sdk_key_empty():
52-
"""Test validation of empty SDK keys"""
53-
logger = Mock(spec=logging.Logger)
47+
def test_is_valid_sdk_key_format_max_length():
48+
"""Test validation of SDK key maximum length"""
49+
valid_key = "a" * 8192
50+
assert is_valid_sdk_key_format(valid_key) is True
5451

55-
assert validate_sdk_key("", logger) is True
56-
logger.warning.assert_not_called()
52+
invalid_key = "a" * 8193
53+
assert is_valid_sdk_key_format(invalid_key) is False

ldclient/testing/test_config.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ def test_sdk_key_validation_valid_keys():
5050
valid_keys = [
5151
"sdk-12345678-1234-1234-1234-123456789012",
5252
"valid-sdk-key-123",
53-
"VALID_SDK_KEY_456"
53+
"VALID_SDK_KEY_456",
54+
"test.key_with.dots",
55+
"test-key-with-hyphens"
5456
]
5557

5658
for key in valid_keys:
@@ -59,25 +61,55 @@ def test_sdk_key_validation_valid_keys():
5961

6062

6163
def test_sdk_key_validation_invalid_keys():
62-
"""Test that invalid SDK keys are rejected"""
64+
"""Test that invalid SDK keys are not set"""
6365
invalid_keys = [
6466
"sdk-key-with-\x00-null",
6567
"sdk-key-with-\n-newline",
6668
"sdk-key-with-\t-tab",
67-
"sdk-key-with-\x7F-del"
69+
"sdk key with spaces",
70+
"sdk@key#with$special%chars",
71+
"sdk/key\\with/slashes"
6872
]
6973

7074
for key in invalid_keys:
71-
with pytest.raises(ValueError, match="SDK key contains invalid characters"):
72-
Config(sdk_key=key)
75+
config = Config(sdk_key=key)
76+
assert config.sdk_key is None
7377

7478

7579
def test_sdk_key_validation_empty_key():
76-
"""Test that empty SDK keys don't trigger format validation"""
80+
"""Test that empty SDK keys are accepted"""
7781
config = Config(sdk_key="")
7882
assert config.sdk_key == ""
7983

8084

85+
def test_sdk_key_validation_none_key():
86+
"""Test that None SDK keys are accepted"""
87+
config = Config(sdk_key=None)
88+
assert config.sdk_key is None
89+
90+
91+
def test_sdk_key_validation_max_length():
92+
"""Test SDK key maximum length validation"""
93+
valid_key = "a" * 8192
94+
config = Config(sdk_key=valid_key)
95+
assert config.sdk_key == valid_key
96+
97+
invalid_key = "a" * 8193
98+
config = Config(sdk_key=invalid_key)
99+
assert config.sdk_key is None
100+
101+
102+
def test_copy_with_new_sdk_key_validation():
103+
"""Test that copy_with_new_sdk_key validates the new key"""
104+
original_config = Config(sdk_key="valid-key")
105+
106+
new_config = original_config.copy_with_new_sdk_key("another-valid-key")
107+
assert new_config.sdk_key == "another-valid-key"
108+
109+
invalid_config = original_config.copy_with_new_sdk_key("invalid key with spaces")
110+
assert invalid_config.sdk_key is None
111+
112+
81113
def application_can_be_set_and_read():
82114
application = {"id": "my-id", "version": "abcdef"}
83115
config = Config(sdk_key="SDK_KEY", application=application)

0 commit comments

Comments
 (0)