{"record":{"id":"b16da8bde9cf8249","repo":"redis/redis-py","slug":"key-must-be-either-a-string-or-bytes","errorCode":null,"errorMessage":"Key must be either a string or bytes","messagePattern":"Key must be either a string or bytes","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"redis/utils.py","lineNumber":355,"sourceCode":"        for _ in range(diff):\n            num_versions1.append(0)\n\n    for i, ver in enumerate(num_versions1):\n        if num_versions1[i] > num_versions2[i]:\n            return -1\n        elif num_versions1[i] < num_versions2[i]:\n            return 1\n\n    return 0\n\n\ndef ensure_string(key):\n    if isinstance(key, bytes):\n        return key.decode(\"utf-8\")\n    elif isinstance(key, str):\n        return key\n    else:\n        raise TypeError(\"Key must be either a string or bytes\")\n\n\ndef extract_expire_flags(\n    ex: Optional[ExpiryT] = None,\n    px: Optional[ExpiryT] = None,\n    exat: Optional[AbsExpiryT] = None,\n    pxat: Optional[AbsExpiryT] = None,\n) -> List[EncodableT]:\n    exp_options: list[EncodableT] = []\n    if ex is not None:\n        exp_options.append(\"EX\")\n        if isinstance(ex, datetime.timedelta):\n            exp_options.append(int(ex.total_seconds()))\n        elif isinstance(ex, int):\n            exp_options.append(ex)\n        elif isinstance(ex, str) and ex.isdigit():\n            exp_options.append(int(ex))\n        else:","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/utils.py#L337-L373","documentation":"Raised by ensure_string() (redis/utils.py:355) as a TypeError. ensure_string() normalizes a value to str: bytes are decoded to utf-8, str passes through, and anything else raises TypeError. In the shipped code it is called by the client-side-caching connection wrapper (redis/connection.py:1736-1737) on the `server` and `version` fields pulled from the Redis HANDSHAKE metadata. A None value is already excluded at connection.py:1733, so this error means the handshake metadata returned a non-string, non-bytes scalar such as an int, bool, or a structured RESP3 object.","triggerScenarios":"Connecting with client-side caching enabled (Redis 7.4+ CSC, protocol=3), the HANDSHAKE metadata's `server` or `version` field arrives decoded as a non-string type (e.g. an integer, a boolean, or a nested aggregate from a non-conforming RESP3 parser path); ensure_string() then raises TypeError before the version-compatibility check at connection.py:1739-1745.","commonSituations":"Pointing the CSC client at a Redis fork / proxy / Valkey variant that emits `version` as a numeric or structured value; a RESP3 decoder change that returns the handshake field as a non-string; a custom or in-house server that reshapes the HANDSHAKE `server`/`version` entries.","solutions":["Inspect what the server actually returns for `server`/`version` in the handshake (enable DEBUG logging on redis.connection) and ensure both are strings.","Run a server version whose HANDSHAKE emits string `server`/`version` fields (Redis 7.4+ compliant).","If running a compatible fork, patch it to emit string handshake fields.","Avoid enabling client-side caching against servers with non-conforming handshakes."],"exampleFix":"// before: server returns version as integer in handshake metadata\n// -> TypeError: Key must be either a string or bytes (utils.py:355)\n\n// after: server-side fix - emit HANDSHAKE version as a bulk string\n// server: arr *2\r\n $7\r\nserver\r\n $5\r\nredis\r\n $7\r\nversion\r\n $5\r\n7.4.0\r\n","handlingStrategy":"type-guard","validationCode":"def handshake_fields_are_strings(client) -> bool:\n    md = getattr(getattr(client, 'connection_pool', None), 'handshake_metadata', None)\n    if not md:\n        return True  # nothing to check yet\n    for key in (b'server', 'server', b'version', 'version'):\n        v = md.get(key)\n        if v is not None and not isinstance(v, (str, bytes)):\n            return False\n    return True","typeGuard":"from typing import Union\n\ndef is_str_or_bytes(v) -> bool:\n    return isinstance(v, (str, bytes))\n\n# equivalent to the guard ensure_string() expects:\n# assert is_str_or_bytes(handshake['version']), 'non-string handshake field'\n# assert is_str_or_bytes(handshake['server']), 'non-string handshake field'","tryCatchPattern":"import redis\ntry:\n    c = redis.Redis(host=..., client_side_caching=True, protocol=3)\n    c.ping()\nexcept TypeError as e:\n    if 'must be either a string or bytes' in str(e):\n        # server returned non-string server/version in HANDSHAKE metadata;\n        # use a Redis 7.4+ compliant server or disable client-side caching\n        ...","preventionTips":["Use a Redis 7.4+ (or compliant fork) server whose HANDSHAKE emits string fields.","Smoke-test client_side_caching=True against your exact server build in staging before prod.","If unsure of server handshake shape, leave client-side caching off until verified."],"tags":["client-side-caching","handshake","resp3","type-error"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}