Skip to content

OrderedMapSerializedKey.get() crashes with AttributeError instead of returning default for a non-matching key #946

Description

@scylla-zeus-bot

Description

OrderedMapSerializedKey (cassandra/util.py) implements the collections.abc.Mapping interface, so callers naturally use .get(key) the way they would on a plain dict — expecting it to return None (or a supplied default) for a missing/invalid key. Instead, an unhandled AttributeError escapes from deep inside the CQL type serializer whenever the lookup key fails to serialize with the map's key type, crashing the caller instead of behaving like dict.get().

Steps to reproduce

Any code path that calls .get(key) on a driver-returned map<text, ...> value with a key that isn't a str (most commonly None, e.g. because the caller derived the key from a still-refreshing piece of cluster metadata) triggers the crash:

from cassandra.util import OrderedMapSerializedKey
from cassandra.cqltypes import UTF8Type

m = OrderedMapSerializedKey(UTF8Type, protocol_version=4)
m._insert_unchecked('dc1', UTF8Type.serialize('dc1', 4), 3)

m.get(None)
Traceback (most recent call last):
  File "cassandra/util.py", line 725, in cassandra.util.OrderedMap.__getitem__
  File "cassandra/util.py", line 790, in cassandra.util.OrderedMapSerializedKey._serialize_key
  File "cassandra/cqltypes.py", line 785, in cassandra.cqltypes.UTF8Type.serialize
AttributeError: 'NoneType' object has no attribute 'encode'

Root cause

Mapping.get() (the mixin used by OrderedMap) is implemented roughly as:

def get(self, key, default=None):
    try:
        return self[key]
    except KeyError:
        return default

OrderedMap.__getitem__ looks up self._index[self._serialize_key(key)]. For OrderedMapSerializedKey, _serialize_key calls self.cass_key_type.serialize(key, self.protocol_version) directly (cassandra/util.py:790), with no exception handling. When the key type is UTF8Type and the supplied key is not a string (e.g. None), UTF8Type.serialize (cassandra/cqltypes.py:782-788) only catches UnicodeDecodeError — it lets AttributeError (from None.encode(...)) or other serialization errors propagate unchanged:

@staticmethod
def serialize(ustr, protocol_version):
    try:
        return ustr.encode('utf-8')
    except UnicodeDecodeError:
        # already utf-8
        return ustr

Because _serialize_key doesn't translate serialization failures into KeyError, Mapping.get()'s except KeyError never fires, and the raw AttributeError (or whatever the underlying type's serialize() happens to raise for a bad key) escapes to the caller. This defeats the whole point of using .get() and is surprising for a class that advertises the standard Mapping API.

Expected behavior

OrderedMapSerializedKey.get(key) (and __getitem__/__contains__) should behave like dict.get()/in for any key that cannot possibly be present — i.e. a key that fails to serialize against the map's declared key type should simply be treated as "not found" (KeyError from __getitem__, default from .get(), False from in), rather than letting an unrelated, type-specific low-level exception (AttributeError, or others depending on the CQL type) bubble up.

Suggested fix

In OrderedMapSerializedKey._serialize_key (cassandra/util.py), wrap the call to self.cass_key_type.serialize(...) and translate serialization failures (AttributeError, TypeError, struct.error, etc.) into a KeyError, e.g.:

def _serialize_key(self, key):
    try:
        return self.cass_key_type.serialize(key, self.protocol_version)
    except Exception as exc:
        raise KeyError(key) from exc

This keeps OrderedMap.__getitem__'s existing except KeyError (and therefore Mapping.get()/__contains__) working as documented, for any key type mismatch — not just None.

How we hit this

We hit this in ScyllaDB Cluster Tests (SCT), whose DataCenterTopologyRfControl._get_keyspaces_to_decrease_rf() reads a per-datacenter replication factor from a driver-returned map<text, int> (NetworkTopologyStrategy replication settings) with:

rf = replication.get(self.datacenter)
if rf is None:
    ...  # handle "datacenter not in this keyspace's RF map"

self.datacenter is normally a str, but in a race window while the driver's cluster metadata was mid-refresh (the client observed a "host rediscovery flood" shortly before this), the caller's cached datacenter value resolved to None for one call. The intent of the replication.get(self.datacenter) / is None check was exactly to safely handle a datacenter that isn't a key in the map — but instead of returning None as dict.get() would, the call crashed with:

AttributeError: 'NoneType' object has no attribute 'encode'
  File "sdcm/utils/replication_strategy_utils.py", line 184, in _get_keyspaces_to_decrease_rf
    rf = replication.get(self.datacenter)
  File "cassandra/util.py", line 725, in cassandra.util.OrderedMap.__getitem__
  File "cassandra/util.py", line 790, in cassandra.util.OrderedMapSerializedKey._serialize_key
  File "cassandra/cqltypes.py", line 785, in cassandra.cqltypes.UTF8Type.serialize
AttributeError: 'NoneType' object has no attribute 'encode'

This has recurred multiple times across several longevity_test.LongevityTest.test_custom_time runs against Scylla 2026.3.0~rc0 (internal tracking: SCT-695), always with .get() called with a non-str key on a map<text, ...> result. We are also implementing a client-side guard in SCT (never pass a non-str/None key into .get()), but the driver's .get() should not be able to crash like this regardless of caller input.

Environment

  • cassandra-driver (ScyllaDB python-driver), current master
  • Observed against Scylla 2026.3.0~rc0
  • Python 3.x

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions