Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion mixpanel/flags/local_feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ def __init__(
else:
auth = httpx.BasicAuth(token, "")

scheme = "https" if config.use_https else "http"
httpx_client_parameters = {
"base_url": f"https://{config.api_host}",
"base_url": f"{scheme}://{config.api_host}",
"headers": REQUEST_HEADERS,
"auth": auth,
"timeout": httpx.Timeout(config.request_timeout_in_seconds),
Expand Down
3 changes: 2 additions & 1 deletion mixpanel/flags/remote_feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ def __init__(
else:
auth = httpx.BasicAuth(token, "")

scheme = "https" if config.use_https else "http"
httpx_client_parameters = {
"base_url": f"https://{config.api_host}",
"base_url": f"{scheme}://{config.api_host}",
"headers": REQUEST_HEADERS,
"auth": auth,
"timeout": httpx.Timeout(config.request_timeout_in_seconds),
Expand Down
77 changes: 77 additions & 0 deletions mixpanel/flags/test_local_feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -1094,3 +1094,80 @@ def test_sync_context_manager_exit_closes_both_clients():

assert provider._sync_client.is_closed
assert provider._async_client.is_closed


def _make_provider(**config_kwargs):
config = LocalFlagsConfig(enable_polling=False, **config_kwargs)
return LocalFeatureFlagsProvider("test-token", config, "1.0.0", Mock())


def test_use_https_defaults_to_true():
assert LocalFlagsConfig().use_https is True


def test_use_https_leaves_positional_args_unshifted():
"""use_https must stay last so it can't rebind an existing positional.

Declaring it on FlagsConfig would move enable_polling from the fourth
positional slot to the fifth, so a pre-existing
``LocalFlagsConfig(host, timeout, executor, False)`` would silently leave
polling enabled and disable HTTPS. See the rationale comment in types.py.
"""
config = LocalFlagsConfig("example.com", 5, None, False)

assert config.enable_polling is False
assert config.use_https is True


def test_default_config_uses_https_base_url():
provider = _make_provider()

assert str(provider._sync_client.base_url) == "https://api.mixpanel.com"
assert str(provider._async_client.base_url) == "https://api.mixpanel.com"

provider.shutdown()


def test_explicit_use_https_true_matches_default():
provider = _make_provider(use_https=True)

assert str(provider._sync_client.base_url) == "https://api.mixpanel.com"
assert str(provider._async_client.base_url) == "https://api.mixpanel.com"

provider.shutdown()


def test_use_https_false_uses_http_base_url():
provider = _make_provider(use_https=False)

assert str(provider._sync_client.base_url) == "http://api.mixpanel.com"
assert str(provider._async_client.base_url) == "http://api.mixpanel.com"

provider.shutdown()


def test_use_https_false_builds_full_http_definitions_url():
provider = _make_provider(api_host="host.minikube.internal/tproxy", use_https=False)

for client in (provider._sync_client, provider._async_client):
request = client.build_request(
"GET", LocalFeatureFlagsProvider.FLAGS_DEFINITIONS_URL_PATH
)
assert (
str(request.url) == "http://host.minikube.internal/tproxy/flags/definitions"
)

provider.shutdown()


def test_scheme_headers_stay_https_when_use_https_false():
"""The backend's auth rejects requests not marked as https, so these
headers must not follow the transport scheme (see utils.REQUEST_HEADERS).
"""
provider = _make_provider(use_https=False)

for client in (provider._sync_client, provider._async_client):
assert client.headers["X-Forwarded-Proto"] == "https"
assert client.headers["X-Scheme"] == "https"

provider.shutdown()
59 changes: 59 additions & 0 deletions mixpanel/flags/test_remote_feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,3 +652,62 @@ def test_sync_context_manager_exit_closes_both_clients():

assert provider._sync_client.is_closed
assert provider._async_client.is_closed


def _make_provider(**config_kwargs):
config = RemoteFlagsConfig(**config_kwargs)
return RemoteFeatureFlagsProvider("test-token", config, "1.0.0", Mock())


def test_use_https_defaults_to_true():
assert RemoteFlagsConfig().use_https is True


def test_default_config_uses_https_base_url():
provider = _make_provider()

assert str(provider._sync_client.base_url) == "https://api.mixpanel.com"
assert str(provider._async_client.base_url) == "https://api.mixpanel.com"

provider.shutdown()


def test_explicit_use_https_true_matches_default():
provider = _make_provider(use_https=True)

assert str(provider._sync_client.base_url) == "https://api.mixpanel.com"
assert str(provider._async_client.base_url) == "https://api.mixpanel.com"

provider.shutdown()


def test_use_https_false_uses_http_base_url():
provider = _make_provider(use_https=False)

assert str(provider._sync_client.base_url) == "http://api.mixpanel.com"
assert str(provider._async_client.base_url) == "http://api.mixpanel.com"

provider.shutdown()


def test_use_https_false_builds_full_http_flags_url():
provider = _make_provider(api_host="host.minikube.internal/tproxy", use_https=False)

for client in (provider._sync_client, provider._async_client):
request = client.build_request("GET", RemoteFeatureFlagsProvider.FLAGS_URL_PATH)
assert str(request.url) == "http://host.minikube.internal/tproxy/flags"

provider.shutdown()


def test_scheme_headers_stay_https_when_use_https_false():
"""The backend's auth rejects requests not marked as https, so these
headers must not follow the transport scheme (see utils.REQUEST_HEADERS).
"""
provider = _make_provider(use_https=False)

for client in (provider._sync_client, provider._async_client):
assert client.headers["X-Forwarded-Proto"] == "https"
assert client.headers["X-Scheme"] == "https"

provider.shutdown()
3 changes: 2 additions & 1 deletion mixpanel/flags/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ class FlagsConfig:
class LocalFlagsConfig(FlagsConfig):
enable_polling: bool = True
polling_interval_in_seconds: int = 60
use_https: bool = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just needs to be on the base FlagsConfig



@dataclass
class RemoteFlagsConfig(FlagsConfig):
pass
use_https: bool = True


@dataclass
Expand Down
4 changes: 4 additions & 0 deletions mixpanel/flags/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ def close_async_client_from_sync(client: httpx.AsyncClient) -> None:
)


# The scheme headers are intentionally always "https", even when a provider is
# configured with use_https=False. They describe the original request's scheme
# to the flags backend, whose auth rejects requests not marked as https, so a
# proxy fronting a plain-HTTP dev endpoint still needs to see https here.
REQUEST_HEADERS: dict[str, str] = {
"X-Scheme": "https",
"X-Forwarded-Proto": "https",
Expand Down
Loading