From a5f6d6eb4b35ac08894511bd23bb35df42317aa0 Mon Sep 17 00:00:00 2001 From: Wei Qi Lu Date: Thu, 26 Feb 2026 14:37:50 -0800 Subject: [PATCH 1/4] python(fix): fixed grpc connection errors with missing scheme --- .../_tests/_internal/test_transport.py | 40 +++++++++++++++++++ .../sift_client/transport/grpc_transport.py | 3 ++ 2 files changed, 43 insertions(+) create mode 100644 python/lib/sift_client/_tests/_internal/test_transport.py diff --git a/python/lib/sift_client/_tests/_internal/test_transport.py b/python/lib/sift_client/_tests/_internal/test_transport.py new file mode 100644 index 000000000..0ab5df5be --- /dev/null +++ b/python/lib/sift_client/_tests/_internal/test_transport.py @@ -0,0 +1,40 @@ +"""Tests for URL normalization in GrpcConfig and RestConfig.""" + +from sift_client.transport.grpc_transport import GrpcConfig +from sift_client.transport.rest_transport import RestConfig + + +class TestGrpcConfigUrl: + def test_adds_https_when_missing(self): + config = GrpcConfig(url="grpc.sift.com", api_key="api") + assert config.uri == "https://grpc.sift.com" + + def test_adds_http_when_missing_local(self): + config = GrpcConfig(url="grpc.sift.com", api_key="api", use_ssl=False) + assert config.uri == "http://grpc.sift.com" + + def test_url_keeps_https(self): + config = GrpcConfig(url="https://grpc.sift.com", api_key="api") + assert config.uri == "https://grpc.sift.com" + + def test_url_keeps_http(self): + config = GrpcConfig(url="http://grpc.sift.com", api_key="api", use_ssl=False) + assert config.uri == "http://grpc.sift.com" + + +class TestRestConfigUrl: + def test_adds_https_when_missing(self): + config = RestConfig(base_url="rest.sift.com", api_key="api") + assert config.base_url == "https://rest.sift.com" + + def test_add_http_when_missing_local(self): + config = RestConfig(base_url="rest.sift.com", api_key="api", use_ssl=False) + assert config.base_url == "http://rest.sift.com" + + def test_url_keeps_https(self): + config = RestConfig(base_url="https://rest.sift.com", api_key="api") + assert config.base_url == "https://rest.sift.com" + + def test_url_keeps_http(self): + config = RestConfig(base_url="http://rest.sift.com", api_key="api", use_ssl=False) + assert config.base_url == "http://rest.sift.com" diff --git a/python/lib/sift_client/transport/grpc_transport.py b/python/lib/sift_client/transport/grpc_transport.py index b27ce8fc1..108091d02 100644 --- a/python/lib/sift_client/transport/grpc_transport.py +++ b/python/lib/sift_client/transport/grpc_transport.py @@ -55,6 +55,9 @@ def __init__( use_async: Whether to use async gRPC client. metadata: Additional metadata to include in all requests. """ + if not url.startswith("http"): + # urljoin (used when executing requests) requires URL starting with http or https + url = f"https://{url}" if use_ssl else f"http://{url}" self.uri = url self.api_key = api_key self.use_ssl = use_ssl From 253401f1fdecc3d836a8f4b5599649d590a39c8d Mon Sep 17 00:00:00 2001 From: Wei Qi Lu Date: Mon, 2 Mar 2026 16:03:27 -0800 Subject: [PATCH 2/4] python(fix): fixed grpc connection errors with missing scheme --- .../_tests/_internal/test_transport.py | 27 ++++++++++++++++++- .../sift_client/transport/grpc_transport.py | 14 +++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/python/lib/sift_client/_tests/_internal/test_transport.py b/python/lib/sift_client/_tests/_internal/test_transport.py index 0ab5df5be..31ac1e4ef 100644 --- a/python/lib/sift_client/_tests/_internal/test_transport.py +++ b/python/lib/sift_client/_tests/_internal/test_transport.py @@ -1,5 +1,7 @@ """Tests for URL normalization in GrpcConfig and RestConfig.""" +import pytest + from sift_client.transport.grpc_transport import GrpcConfig from sift_client.transport.rest_transport import RestConfig @@ -8,6 +10,18 @@ class TestGrpcConfigUrl: def test_adds_https_when_missing(self): config = GrpcConfig(url="grpc.sift.com", api_key="api") assert config.uri == "https://grpc.sift.com" + + def test_adds_https_on_localhost(self): + config = GrpcConfig(url="localhost:50051", api_key="api", use_ssl=False) + assert config.uri == "http://localhost:50051" + + def test_adds_https_on_ip(self): + conifg = GrpcConfig(url="129.10.1.1", api_key="api") + assert conifg.uri == "https://129.10.1.1" + + def test_adds_https_on_ipv6(self): + config = GrpcConfig(url="[::]:8080", api_key="api") + assert config.uri == "https://[::]:8080" def test_adds_http_when_missing_local(self): config = GrpcConfig(url="grpc.sift.com", api_key="api", use_ssl=False) @@ -20,7 +34,18 @@ def test_url_keeps_https(self): def test_url_keeps_http(self): config = GrpcConfig(url="http://grpc.sift.com", api_key="api", use_ssl=False) assert config.uri == "http://grpc.sift.com" - + + def test_raises_on_invalid_url(self): + with pytest.raises(ValueError): + GrpcConfig(url="htp://localhost:8080", api_key="api") + + def test_raise_on_invalid_url2(self): + with pytest.raises(ValueError): + GrpcConfig(url="https:/localhost:50051", api_key="api") + + def test_raise_on_missing_url(self): + with pytest.raises(ValueError): + GrpcConfig(url="", api_key="api") class TestRestConfigUrl: def test_adds_https_when_missing(self): diff --git a/python/lib/sift_client/transport/grpc_transport.py b/python/lib/sift_client/transport/grpc_transport.py index 108091d02..eca893678 100644 --- a/python/lib/sift_client/transport/grpc_transport.py +++ b/python/lib/sift_client/transport/grpc_transport.py @@ -11,6 +11,7 @@ import logging import threading from typing import Any +from urllib.parse import urlparse from sift_py.grpc.transport import ( SiftChannelConfig, @@ -55,10 +56,15 @@ def __init__( use_async: Whether to use async gRPC client. metadata: Additional metadata to include in all requests. """ - if not url.startswith("http"): - # urljoin (used when executing requests) requires URL starting with http or https - url = f"https://{url}" if use_ssl else f"http://{url}" - self.uri = url + parsed_url = urlparse(url) + normalized_url = url + if not parsed_url.netloc and parsed_url.scheme not in ("http", "https"): + # missing netloc means no '://' separator and will prepend the scheme + normalized_url = f"https://{url}" if use_ssl else f"http://{url}" + parsed_url = urlparse(normalized_url) + if parsed_url.scheme not in ("http", "https") or not parsed_url.netloc: + raise ValueError(f"Invalid connection URL '{url}'. Expected format: 'http[s]://hostname[:port]'.") + self.uri = normalized_url self.api_key = api_key self.use_ssl = use_ssl self.cert_via_openssl = cert_via_openssl From 6922fcb3b2438c427c63bd47ca47a2d7a9f71c99 Mon Sep 17 00:00:00 2001 From: Wei Qi Lu Date: Mon, 2 Mar 2026 16:08:49 -0800 Subject: [PATCH 3/4] python(fix) - linting --- .../sift_client/_tests/_internal/test_transport.py | 13 +++++++------ python/lib/sift_client/transport/grpc_transport.py | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/python/lib/sift_client/_tests/_internal/test_transport.py b/python/lib/sift_client/_tests/_internal/test_transport.py index 31ac1e4ef..a27f6c59a 100644 --- a/python/lib/sift_client/_tests/_internal/test_transport.py +++ b/python/lib/sift_client/_tests/_internal/test_transport.py @@ -10,15 +10,15 @@ class TestGrpcConfigUrl: def test_adds_https_when_missing(self): config = GrpcConfig(url="grpc.sift.com", api_key="api") assert config.uri == "https://grpc.sift.com" - + def test_adds_https_on_localhost(self): config = GrpcConfig(url="localhost:50051", api_key="api", use_ssl=False) assert config.uri == "http://localhost:50051" - + def test_adds_https_on_ip(self): conifg = GrpcConfig(url="129.10.1.1", api_key="api") assert conifg.uri == "https://129.10.1.1" - + def test_adds_https_on_ipv6(self): config = GrpcConfig(url="[::]:8080", api_key="api") assert config.uri == "https://[::]:8080" @@ -34,19 +34,20 @@ def test_url_keeps_https(self): def test_url_keeps_http(self): config = GrpcConfig(url="http://grpc.sift.com", api_key="api", use_ssl=False) assert config.uri == "http://grpc.sift.com" - + def test_raises_on_invalid_url(self): with pytest.raises(ValueError): GrpcConfig(url="htp://localhost:8080", api_key="api") - + def test_raise_on_invalid_url2(self): with pytest.raises(ValueError): GrpcConfig(url="https:/localhost:50051", api_key="api") - + def test_raise_on_missing_url(self): with pytest.raises(ValueError): GrpcConfig(url="", api_key="api") + class TestRestConfigUrl: def test_adds_https_when_missing(self): config = RestConfig(base_url="rest.sift.com", api_key="api") diff --git a/python/lib/sift_client/transport/grpc_transport.py b/python/lib/sift_client/transport/grpc_transport.py index eca893678..dedbba934 100644 --- a/python/lib/sift_client/transport/grpc_transport.py +++ b/python/lib/sift_client/transport/grpc_transport.py @@ -58,12 +58,14 @@ def __init__( """ parsed_url = urlparse(url) normalized_url = url - if not parsed_url.netloc and parsed_url.scheme not in ("http", "https"): + if not parsed_url.netloc and parsed_url.scheme not in ("http", "https"): # missing netloc means no '://' separator and will prepend the scheme normalized_url = f"https://{url}" if use_ssl else f"http://{url}" parsed_url = urlparse(normalized_url) if parsed_url.scheme not in ("http", "https") or not parsed_url.netloc: - raise ValueError(f"Invalid connection URL '{url}'. Expected format: 'http[s]://hostname[:port]'.") + raise ValueError( + f"Invalid connection URL '{url}'. Expected format: 'http[s]://hostname[:port]'." + ) self.uri = normalized_url self.api_key = api_key self.use_ssl = use_ssl From 97a7071a8c9079a3734e11a9cc62c6dbc8c744fb Mon Sep 17 00:00:00 2001 From: Wei Qi Lu Date: Mon, 2 Mar 2026 16:11:39 -0800 Subject: [PATCH 4/4] python(fix): more linting --- python/lib/sift_client/_tests/_internal/test_transport.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lib/sift_client/_tests/_internal/test_transport.py b/python/lib/sift_client/_tests/_internal/test_transport.py index a27f6c59a..bee6d29c6 100644 --- a/python/lib/sift_client/_tests/_internal/test_transport.py +++ b/python/lib/sift_client/_tests/_internal/test_transport.py @@ -36,15 +36,15 @@ def test_url_keeps_http(self): assert config.uri == "http://grpc.sift.com" def test_raises_on_invalid_url(self): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Invalid connection URL"): GrpcConfig(url="htp://localhost:8080", api_key="api") def test_raise_on_invalid_url2(self): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Invalid connection URL"): GrpcConfig(url="https:/localhost:50051", api_key="api") def test_raise_on_missing_url(self): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Invalid connection URL"): GrpcConfig(url="", api_key="api")