Changing the default protocol used to RESP3 keeping the responses for default config compatible with current RESP2 shape. Adding possibility to opt-in for unified responses for both protocol.#4052
Conversation
🛡️ Jit Security Scan Results✅ No security findings were detected in this PR
Security scan by Jit
|
This reverts commit de8a47a.
The default RESP protocol on the wire is now RESP3 (was RESP2). To keep
existing applications working unchanged, response shapes still match
today's RESP2-style Python objects by default, controlled by a new
legacy_responses=True parameter on the public clients.
* redis/utils.py
- DEFAULT_RESP_VERSION lives here (moved from redis.connection) and is
set to 3
- check_protocol_version() resolves protocol=None to
DEFAULT_RESP_VERSION before comparison, so a missing protocol is
treated as RESP3-capable
* redis/connection.py, redis/asyncio/connection.py
- import DEFAULT_RESP_VERSION from redis.utils (still re-exported from
redis.connection for backward compatibility)
- AbstractConnection.__init__: protocol default 2 -> None, new
legacy_responses: bool = True stored as self.legacy_responses
- URL_QUERY_ARGUMENT_PARSERS accepts protocol and
legacy_responses, so both can be passed in connection URLs via
redis://...?protocol=3&legacy_responses=false
* redis/client.py, redis/asyncio/client.py
- Redis.__init__: protocol default 2 -> None, new legacy_responses
parameter, both forwarded into the connection pool kwargs
- client-side caching and maintenance-notifications gating switched
to check_protocol_version(...) so a missing protocol is treated
as RESP3
* redis/asyncio/cluster.py
- RedisCluster.__init__: protocol default 2 -> None, new
legacy_responses parameter, both placed in the cluster connection
kwargs
* redis/cluster.py
- REDIS_ALLOWED_KEYS includes legacy_responses so it propagates
through cleanup_kwargs() to per-node Redis instances
A user-supplied protocol=None is preserved on the pool / connection
kwargs; only the wire-level HELLO handshake resolves it to the concrete
DEFAULT_RESP_VERSION.
…s. Updating some of the unified forms to the correct ones
f66c34a to
0f76634
Compare
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
|
@sera bypass |
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 05742f8. Configure here.
| def parse_sentinel_master_unified_resp3(response, **options): | ||
| state = parse_sentinel_state_resp3(response, **options) | ||
| _add_derived_sentinel_booleans(state, state["flags"]) | ||
| return state |
There was a problem hiding this comment.
Redundant _add_derived_sentinel_booleans calls in unified RESP3 parsers
Low Severity
parse_sentinel_master_unified_resp3, parse_sentinel_masters_unified_resp3, and parse_sentinel_slaves_and_sentinels_unified_resp3 all call _add_derived_sentinel_booleans(state, state["flags"]) after parse_sentinel_state_resp3 already calls _add_derived_sentinel_booleans(result, flags) internally. The second call is completely redundant — it overwrites the same boolean keys with the same values. This adds confusion about whether the inner call does something different from the outer call.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 05742f8. Configure here.
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
|
@sera bypass |
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
|
@sera bypass |
|
@petyaslavova Could you please also update |
❌ Jit Scanner failed - Our team is investigatingJit Scanner failed - Our team has been notified and is working to resolve the issue. Please contact support if you have any questions. 💡 Need to bypass this check? Comment |
|
@sera bypass |
… default config compatible with current RESP2 shape. Adding possibility to opt-in for unified responses for both protocol. (#4052) * Revert "Changing the default RESP protocol configurations for Redis clients and connections to 3 (#4031)" This reverts commit 0984334. * Revert "RESP2 and RESP3 response unification (#4025)" This reverts commit de8a47a. * Fixing newly added test to be compatible with the old style responses * Default to RESP3 wire protocol, add legacy_responses opt-out The default RESP protocol on the wire is now RESP3 (was RESP2). To keep existing applications working unchanged, response shapes still match today's RESP2-style Python objects by default, controlled by a new legacy_responses=True parameter on the public clients. * redis/utils.py - DEFAULT_RESP_VERSION lives here (moved from redis.connection) and is set to 3 - check_protocol_version() resolves protocol=None to DEFAULT_RESP_VERSION before comparison, so a missing protocol is treated as RESP3-capable * redis/connection.py, redis/asyncio/connection.py - import DEFAULT_RESP_VERSION from redis.utils (still re-exported from redis.connection for backward compatibility) - AbstractConnection.__init__: protocol default 2 -> None, new legacy_responses: bool = True stored as self.legacy_responses - URL_QUERY_ARGUMENT_PARSERS accepts protocol and legacy_responses, so both can be passed in connection URLs via redis://...?protocol=3&legacy_responses=false * redis/client.py, redis/asyncio/client.py - Redis.__init__: protocol default 2 -> None, new legacy_responses parameter, both forwarded into the connection pool kwargs - client-side caching and maintenance-notifications gating switched to check_protocol_version(...) so a missing protocol is treated as RESP3 * redis/asyncio/cluster.py - RedisCluster.__init__: protocol default 2 -> None, new legacy_responses parameter, both placed in the cluster connection kwargs * redis/cluster.py - REDIS_ALLOWED_KEYS includes legacy_responses so it propagates through cleanup_kwargs() to per-node Redis instances A user-supplied protocol=None is preserved on the pool / connection kwargs; only the wire-level HELLO handshake resolves it to the concrete DEFAULT_RESP_VERSION. * Adding test fixes * Adding changes for first several commands and test helpers * Adding changes for batches 5 and 6 * Adding the rest of the base commands. * Adding changes for probabilistic and timeseries modules * Adding changes for search and json modules. Adding docs and test fixes. Updating some of the unified forms to the correct ones * Adding test_xreadgroup_with_claim_min_idle_time fix * Adding pipeline test coverage for all combinations of protocol and legacy_responses * Applying review comments * Applying review comments * Applying review comments and fixing failing tests * Applying review comments and fixing failing tests * Add flaky test fix * Add flaky test fix- second try, same test - fails for just one job * Fixing unstable in CI tests * Applying review comments * Fixing linters and spelling error * Applying review comments
## Issue The vector set Python tests intentionally use two clients: - the default client (`self.redis`) for the existing RESP2-oriented test expectations - `self.redis3` for RESP3-specific coverage. However, the default client did not explicitly set a protocol, so it depended on redis-py's default behavior. With newer redis-py versions, RESP3 is now the default protocol(redis/redis-py#4052). In particular, vector set replies such as `VSIM ... WITHSCORES` may be parsed into map/dict-like structures instead of the RESP2 flat-array shape assumed by existing tests. ## Changes Explicitly create the default primary and replica Redis clients with `protocol=2`. `self.redis3` is left unchanged and continues to use `protocol=3` for RESP3-specific test coverage.
## Issue The vector set Python tests intentionally use two clients: - the default client (`self.redis`) for the existing RESP2-oriented test expectations - `self.redis3` for RESP3-specific coverage. However, the default client did not explicitly set a protocol, so it depended on redis-py's default behavior. With newer redis-py versions, RESP3 is now the default protocol(redis/redis-py#4052). In particular, vector set replies such as `VSIM ... WITHSCORES` may be parsed into map/dict-like structures instead of the RESP2 flat-array shape assumed by existing tests. ## Changes Explicitly create the default primary and replica Redis clients with `protocol=2`. `self.redis3` is left unchanged and continues to use `protocol=3` for RESP3-specific test coverage. (cherry picked from commit 8fcf3dc)
## Issue The vector set Python tests intentionally use two clients: - the default client (`self.redis`) for the existing RESP2-oriented test expectations - `self.redis3` for RESP3-specific coverage. However, the default client did not explicitly set a protocol, so it depended on redis-py's default behavior. With newer redis-py versions, RESP3 is now the default protocol(redis/redis-py#4052). In particular, vector set replies such as `VSIM ... WITHSCORES` may be parsed into map/dict-like structures instead of the RESP2 flat-array shape assumed by existing tests. ## Changes Explicitly create the default primary and replica Redis clients with `protocol=2`. `self.redis3` is left unchanged and continues to use `protocol=3` for RESP3-specific test coverage. (cherry picked from commit 8fcf3dc)
…4109) * fix(search): parse RESP3 FT.SEARCH responses with bytes-typed keys Since the wire protocol default switched to RESP3 (#4052), the server returns FT.SEARCH responses as RESP3 maps. When a client is opened with ``decode_responses=False`` the map keys arrive as ``bytes`` rather than ``str``, but ``Result.from_resp3`` looked them up as plain strings: instance.total = res.get("total_results", 0) for result_item in res.get("results", []): ... Because ``b"total_results" != "total_results"``, every lookup missed and the search appeared to return ``Result{0 total, docs: []}`` even though the server had matched documents. Normalise the top-level map and each per-result map to string keys before reading them, mirroring the pattern already used by ``_parse_hybrid_search_resp3`` in ``redis/commands/search/commands.py`` ("Top-level keys are normalised to strings"). Adds ``tests/test_search_result.py`` with regression tests covering str-keyed, bytes-keyed, and mixed maps, plus the empty/None edge cases. The tests fail on the unfixed code for the bytes and mixed cases. Fixes #4107 * fix(search): extend bytes-key normalisation to AGGREGATE and SPELLCHECK The RESP3 callbacks for FT.SEARCH (`Result.from_resp3`) were taught to normalise top-level structural map keys to strings so that responses parsed correctly on connections opened with `decode_responses=False`. `_parse_aggregate_resp3` (FT.AGGREGATE / FT.CURSOR READ / FT.PROFILE AGGREGATE) and `_parse_spellcheck_resp3` (FT.SPELLCHECK) still read `"total_results"`, `"results"` and `"warning"` as plain strings, so a byte-keyed RESP3 response missed every lookup and silently parsed as an empty AggregateResult / `{}` even when the server had returned data. Apply the same `str_if_bytes` normalisation that `Result.from_resp3` and `_parse_hybrid_search_resp3` already use: - normalise the top-level map and (for aggregate) the per-result-item map; document data inside `extra_attributes` is left as-is so the caller still sees bytes when `decode_responses=False`, mirroring the RESP2 shape; - normalise the outer `results` key for spellcheck; the inner term keys match the RESP2 `decode_responses=False` shape and stay as bytes. Adds regression tests for both parsers in `tests/test_search_result.py`, plus integration tests in `tests/test_search.py` that exercise the three affected Search callbacks (FT.SEARCH, FT.AGGREGATE, FT.SPELLCHECK) against a real RESP3 wire with a `decode_responses=False` client. * fix(search): apply petyaslavova review feedback - _parse_spellcheck_resp3 now preserves the suggestion value as-is so it keeps the decode_responses shape RESP2 would produce (str when decoded, bytes otherwise) instead of wrapping bytes in str(). - waitForIndex now accepts both str and bytes structural keys in FT.INFO responses. execute_command bypasses the search module's callbacks, so the helper has to handle the raw RESP3 dict/RESP2 list shapes for decode_responses=False clients. This unblocks the previously failing fixed-clients CI matrix entry. - The bytes-keys integration tests are now parametrised over protocol=2 (anchors the legacy output shape) and the default protocol (the path that actually exercises the changed parsers in _RedisCallbacksRESP3toRESP2Legacy). Explicit protocol=3 was routing through _RedisCallbacksRESP3 and bypassing the fix. - Spellcheck assertion is stricter: it pins the term key to b"impornant" and the suggestion value to b"important". - Mirror the suggestion-bytes assertion in the test_search_result.py unit test. * test(search): tidy review nits in RESP3 bytes-key tests - Rephrase the parametrisation comment so it explains *why* the two protocol arms exist in terms of the parsers being exercised (_parse_search_resp3 / _parse_aggregate_resp3 / _parse_spellcheck_resp3) rather than "the changed methods", which was only meaningful relative to this PR's diff. - Reorder decorators on TestSearchResp3BytesKeys methods so the test-scope marks (redismod, fixed_client) stay grouped and @pytest.mark.parametrize sits last, matching the prevailing style for parametrised tests. --------- Co-authored-by: petyaslavova <[email protected]>
…4109) * fix(search): parse RESP3 FT.SEARCH responses with bytes-typed keys Since the wire protocol default switched to RESP3 (#4052), the server returns FT.SEARCH responses as RESP3 maps. When a client is opened with ``decode_responses=False`` the map keys arrive as ``bytes`` rather than ``str``, but ``Result.from_resp3`` looked them up as plain strings: instance.total = res.get("total_results", 0) for result_item in res.get("results", []): ... Because ``b"total_results" != "total_results"``, every lookup missed and the search appeared to return ``Result{0 total, docs: []}`` even though the server had matched documents. Normalise the top-level map and each per-result map to string keys before reading them, mirroring the pattern already used by ``_parse_hybrid_search_resp3`` in ``redis/commands/search/commands.py`` ("Top-level keys are normalised to strings"). Adds ``tests/test_search_result.py`` with regression tests covering str-keyed, bytes-keyed, and mixed maps, plus the empty/None edge cases. The tests fail on the unfixed code for the bytes and mixed cases. Fixes #4107 * fix(search): extend bytes-key normalisation to AGGREGATE and SPELLCHECK The RESP3 callbacks for FT.SEARCH (`Result.from_resp3`) were taught to normalise top-level structural map keys to strings so that responses parsed correctly on connections opened with `decode_responses=False`. `_parse_aggregate_resp3` (FT.AGGREGATE / FT.CURSOR READ / FT.PROFILE AGGREGATE) and `_parse_spellcheck_resp3` (FT.SPELLCHECK) still read `"total_results"`, `"results"` and `"warning"` as plain strings, so a byte-keyed RESP3 response missed every lookup and silently parsed as an empty AggregateResult / `{}` even when the server had returned data. Apply the same `str_if_bytes` normalisation that `Result.from_resp3` and `_parse_hybrid_search_resp3` already use: - normalise the top-level map and (for aggregate) the per-result-item map; document data inside `extra_attributes` is left as-is so the caller still sees bytes when `decode_responses=False`, mirroring the RESP2 shape; - normalise the outer `results` key for spellcheck; the inner term keys match the RESP2 `decode_responses=False` shape and stay as bytes. Adds regression tests for both parsers in `tests/test_search_result.py`, plus integration tests in `tests/test_search.py` that exercise the three affected Search callbacks (FT.SEARCH, FT.AGGREGATE, FT.SPELLCHECK) against a real RESP3 wire with a `decode_responses=False` client. * fix(search): apply petyaslavova review feedback - _parse_spellcheck_resp3 now preserves the suggestion value as-is so it keeps the decode_responses shape RESP2 would produce (str when decoded, bytes otherwise) instead of wrapping bytes in str(). - waitForIndex now accepts both str and bytes structural keys in FT.INFO responses. execute_command bypasses the search module's callbacks, so the helper has to handle the raw RESP3 dict/RESP2 list shapes for decode_responses=False clients. This unblocks the previously failing fixed-clients CI matrix entry. - The bytes-keys integration tests are now parametrised over protocol=2 (anchors the legacy output shape) and the default protocol (the path that actually exercises the changed parsers in _RedisCallbacksRESP3toRESP2Legacy). Explicit protocol=3 was routing through _RedisCallbacksRESP3 and bypassing the fix. - Spellcheck assertion is stricter: it pins the term key to b"impornant" and the suggestion value to b"important". - Mirror the suggestion-bytes assertion in the test_search_result.py unit test. * test(search): tidy review nits in RESP3 bytes-key tests - Rephrase the parametrisation comment so it explains *why* the two protocol arms exist in terms of the parsers being exercised (_parse_search_resp3 / _parse_aggregate_resp3 / _parse_spellcheck_resp3) rather than "the changed methods", which was only meaningful relative to this PR's diff. - Reorder decorators on TestSearchResp3BytesKeys methods so the test-scope marks (redismod, fixed_client) stay grouped and @pytest.mark.parametrize sits last, matching the prevailing style for parametrised tests. --------- Co-authored-by: petyaslavova <[email protected]>


Summary
This PR changes redis-py 8.0 to use RESP3 on the wire by default while preserving legacy RESP2-compatible Python response shapes unless users explicitly opt into unified responses with
legacy_responses=False. It adds the protocol/response-shape routing needed for the full matrix: default protocol(when it is not provided empty string is used to identify the case when we should use resp3 wire communication and return the responses in the legacy resp2 shape),protocol=2, andprotocol=3, each with legacy and unified response modes.The unified response implementation is expanded across core commands, cluster commands, JSON, Search, probabilistic commands, TimeSeries, and VectorSet. Response callbacks now normalize RESP2 and RESP3 wire replies into the approved unified shapes, while keeping legacy RESP2 and explicit native RESP3 behavior intact. Type hints and parser behavior were updated where unified responses introduce new structures.
Tests were updated to assert expected shapes using shared helpers for RESP2 legacy, RESP3 legacy, and unified responses. The test tasks now accept protocol and legacy-response combinations, emit distinct coverage/JUnit artifact names, and include matrix support for running all protocol/legacy combinations. This also includes fixes for cluster/default response configuration issues and a timing-sensitive
XREADGROUP ... CLAIMtest expectation.Docs were refreshed for end users: README and RESP3 docs now describe the redis-py 8.0 defaults, and a new unified responses migration guide explains how to enable unified responses and what changes to expect. The old RESP unification docs/specs were replaced with unified-response-focused migration material.
protocolsettinglegacy_responsessettingTrue)Falseprotocol=2True)protocol=2Falseprotocol=3True)protocol=3FalseNote
High Risk
High risk because it changes default wire protocol behavior and introduces new callback routing that can affect response shapes across many core/module commands, plus expands CI coverage matrix that may surface previously untested incompatibilities.
Overview
Default behavior changes: the client now uses RESP3 on the wire by default while keeping legacy RESP2-compatible Python response shapes unless users opt into unified shapes via
legacy_responses=False.Response parsing/routing: introduces a centralized
get_response_callbacks()selector (redis/_parsers/response_callbacks.py) and adds/adjusts parsers inredis/_parsers/helpers.pyto support three modes: RESP2 legacy, RESP3 native, and unified responses (including RESP3→RESP2 legacy adapters for commands where RESP3 wire types differ).CI + docs updates: GitHub Actions test runner now accepts
<protocol>-<response-shape>-<topology>and runs a full matrix (default/2/3 × legacy/unified × standalone/cluster); documentation is refreshed to explain the new defaults and adds a newdocs/unified_responses.rstmigration guide while replacing the old RESP unification doc.Reviewed by Cursor Bugbot for commit 76a465b. Bugbot is set up for automated code reviews on this repo. Configure here.