From 0debd80e0bfa2d976ef3d07ac0099d51109e0868 Mon Sep 17 00:00:00 2001 From: Arunendra21 <156455722+Arunendra21@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:27:17 +0530 Subject: [PATCH 1/3] fix(asyncio): reject connection URLs missing the scheme separator The async parse_url used urlparse(url).scheme to validate the scheme, which accepts a URL that has a valid scheme name but no "://" separator, for example "redis:foo.bar.com:12345". In that case urlparse reports the scheme as "redis" with an empty netloc, so the function returned partial kwargs and the client silently connected to the default host instead of raising. The sync parse_url already guards against this up front and has a test for it (test_invalid_scheme_raises_error_when_double_slash_missing), but the async copy did not. This adds the same up-front check so both behave identically, and drops the now-unreachable trailing else branch. Adds the matching async test. Co-authored-by: eeshsaxena --- redis/asyncio/connection.py | 18 +++++++++++------- tests/test_asyncio/test_connection_pool.py | 8 ++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/redis/asyncio/connection.py b/redis/asyncio/connection.py index d9048948eb..4e510f773c 100644 --- a/redis/asyncio/connection.py +++ b/redis/asyncio/connection.py @@ -1766,6 +1766,16 @@ class ConnectKwargs(TypedDict, total=False): def parse_url(url: str) -> ConnectKwargs: + if not ( + url.startswith("redis://") + or url.startswith("rediss://") + or url.startswith("unix://") + ): + raise ValueError( + "Redis URL must specify one of the following schemes " + "(redis://, rediss://, unix://)" + ) + parsed: ParseResult = urlparse(url) kwargs: ConnectKwargs = {} @@ -1795,7 +1805,7 @@ def parse_url(url: str) -> ConnectKwargs: kwargs["path"] = unquote(parsed.path) kwargs["connection_class"] = UnixDomainSocketConnection - elif parsed.scheme in ("redis", "rediss"): + else: # implied: parsed.scheme in ("redis", "rediss") if parsed.hostname: kwargs["host"] = unquote(parsed.hostname) if parsed.port: @@ -1812,12 +1822,6 @@ def parse_url(url: str) -> ConnectKwargs: if parsed.scheme == "rediss": kwargs["connection_class"] = SSLConnection - else: - valid_schemes = "redis://, rediss://, unix://" - raise ValueError( - f"Redis URL must specify one of the following schemes ({valid_schemes})" - ) - return kwargs diff --git a/tests/test_asyncio/test_connection_pool.py b/tests/test_asyncio/test_connection_pool.py index 40a5edaa9f..408d90e1b7 100644 --- a/tests/test_asyncio/test_connection_pool.py +++ b/tests/test_asyncio/test_connection_pool.py @@ -662,6 +662,14 @@ def test_invalid_scheme_raises_error(self): "(redis://, rediss://, unix://)" ) + def test_invalid_scheme_raises_error_when_double_slash_missing(self): + with pytest.raises(ValueError) as cm: + redis.ConnectionPool.from_url("redis:foo.bar.com:12345") + assert str(cm.value) == ( + "Redis URL must specify one of the following schemes " + "(redis://, rediss://, unix://)" + ) + @pytest.mark.fixed_client class TestBlockingConnectionPoolURLParsing: From ac09162f33146db9bcf43713151fad4ba0504e6f Mon Sep 17 00:00:00 2001 From: Arunendra21 <156455722+Arunendra21@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:48:11 +0530 Subject: [PATCH 2/3] Address review: accept case-insensitive URL schemes The initial fix used a case-sensitive prefix check, which regressed async parse_url by rejecting mixed-case schemes like "REDIS://host" that urlparse() previously normalized and accepted. Normalize the URL with .lower() before the prefix check so "REDIS://", "Rediss://", and "UNIX://" are accepted again, while still requiring the "://" separator so "redis:foo" is rejected. The same change is applied to the sync parse_url so the two implementations stay consistent. Adds a test that an uppercase scheme is accepted, for both sync and async. Co-authored-by: eeshsaxena --- redis/asyncio/connection.py | 9 ++++----- redis/connection.py | 9 ++++----- tests/test_asyncio/test_connection_pool.py | 6 ++++++ tests/test_connection_pool.py | 6 ++++++ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/redis/asyncio/connection.py b/redis/asyncio/connection.py index 4e510f773c..2935c81cd8 100644 --- a/redis/asyncio/connection.py +++ b/redis/asyncio/connection.py @@ -1766,11 +1766,10 @@ class ConnectKwargs(TypedDict, total=False): def parse_url(url: str) -> ConnectKwargs: - if not ( - url.startswith("redis://") - or url.startswith("rediss://") - or url.startswith("unix://") - ): + # Scheme names are case-insensitive (RFC 3986), so normalize before the + # prefix check; the "://" is required so a URL like "redis:foo" (which + # urlparse would still report as the "redis" scheme) is rejected. + if not url.lower().startswith(("redis://", "rediss://", "unix://")): raise ValueError( "Redis URL must specify one of the following schemes " "(redis://, rediss://, unix://)" diff --git a/redis/connection.py b/redis/connection.py index 9762663432..0488a79a4b 100644 --- a/redis/connection.py +++ b/redis/connection.py @@ -2328,11 +2328,10 @@ def parse_ssl_verify_flags(value): def parse_url(url): - if not ( - url.startswith("redis://") - or url.startswith("rediss://") - or url.startswith("unix://") - ): + # Scheme names are case-insensitive (RFC 3986), so normalize before the + # prefix check; the "://" is required so a URL like "redis:foo" (which + # urlparse would still report as the "redis" scheme) is rejected. + if not url.lower().startswith(("redis://", "rediss://", "unix://")): raise ValueError( "Redis URL must specify one of the following " "schemes (redis://, rediss://, unix://)" diff --git a/tests/test_asyncio/test_connection_pool.py b/tests/test_asyncio/test_connection_pool.py index 408d90e1b7..69c8ef59da 100644 --- a/tests/test_asyncio/test_connection_pool.py +++ b/tests/test_asyncio/test_connection_pool.py @@ -670,6 +670,12 @@ def test_invalid_scheme_raises_error_when_double_slash_missing(self): "(redis://, rediss://, unix://)" ) + def test_uppercase_scheme_is_accepted(self): + # URL schemes are case-insensitive (RFC 3986) + pool = redis.ConnectionPool.from_url("REDIS://my.host") + assert pool.connection_class == redis.Connection + assert_kwargs_subset(pool.connection_kwargs, {"host": "my.host"}) + @pytest.mark.fixed_client class TestBlockingConnectionPoolURLParsing: diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index 662f3270ee..68beda3a7f 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -611,6 +611,12 @@ def test_invalid_scheme_raises_error_when_double_slash_missing(self): "(redis://, rediss://, unix://)" ) + def test_uppercase_scheme_is_accepted(self): + # URL schemes are case-insensitive (RFC 3986) + pool = redis.ConnectionPool.from_url("REDIS://my.host") + assert pool.connection_class == redis.Connection + assert_kwargs_subset(pool.connection_kwargs, {"host": "my.host"}) + @pytest.mark.fixed_client class TestBlockingConnectionPoolURLParsing: From 6460ba3ef912015b120cde3027c59c4d07b24d24 Mon Sep 17 00:00:00 2001 From: Arunendra21 <156455722+Arunendra21@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:16:02 +0530 Subject: [PATCH 3/3] Extend uppercase-scheme tests to REDISS:// and UNIX:// Per review, cover the TLS and unix-socket variants of the case-insensitive scheme handling in both the sync and async connection-pool URL-parsing tests, asserting SSLConnection and UnixDomainSocketConnection respectively. Co-authored-by: eeshsaxena --- tests/test_asyncio/test_connection_pool.py | 8 ++++++++ tests/test_connection_pool.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/test_asyncio/test_connection_pool.py b/tests/test_asyncio/test_connection_pool.py index 69c8ef59da..a809b67e04 100644 --- a/tests/test_asyncio/test_connection_pool.py +++ b/tests/test_asyncio/test_connection_pool.py @@ -676,6 +676,14 @@ def test_uppercase_scheme_is_accepted(self): assert pool.connection_class == redis.Connection assert_kwargs_subset(pool.connection_kwargs, {"host": "my.host"}) + ssl_pool = redis.ConnectionPool.from_url("REDISS://my.host") + assert ssl_pool.connection_class == redis.SSLConnection + assert_kwargs_subset(ssl_pool.connection_kwargs, {"host": "my.host"}) + + unix_pool = redis.ConnectionPool.from_url("UNIX:///tmp/redis.sock") + assert unix_pool.connection_class == redis.UnixDomainSocketConnection + assert_kwargs_subset(unix_pool.connection_kwargs, {"path": "/tmp/redis.sock"}) + @pytest.mark.fixed_client class TestBlockingConnectionPoolURLParsing: diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index 68beda3a7f..2a538ba84e 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -617,6 +617,14 @@ def test_uppercase_scheme_is_accepted(self): assert pool.connection_class == redis.Connection assert_kwargs_subset(pool.connection_kwargs, {"host": "my.host"}) + ssl_pool = redis.ConnectionPool.from_url("REDISS://my.host") + assert ssl_pool.connection_class == redis.SSLConnection + assert_kwargs_subset(ssl_pool.connection_kwargs, {"host": "my.host"}) + + unix_pool = redis.ConnectionPool.from_url("UNIX:///tmp/redis.sock") + assert unix_pool.connection_class == redis.UnixDomainSocketConnection + assert_kwargs_subset(unix_pool.connection_kwargs, {"path": "/tmp/redis.sock"}) + @pytest.mark.fixed_client class TestBlockingConnectionPoolURLParsing: