Skip to content

FT.SEARCH reply parser crashes on a nil field-array (TypeError: 'NoneType' object is not subscriptable) #38

Description

@vishal-bala

Summary

Executor's FT.SEARCH reply parsing assumes every per-document field-array in the reply is a list and slices it directly. When one document's field-array comes back as None, dict(zip(row_data[::2], row_data[1::2])) raises TypeError: 'NoneType' object is not subscriptable and the entire query fails.

Where

sql_redis/executor.py, the standard (non-WITHSCORES) FT.SEARCH branch:

# Standard format: [count, key1, [fields1], key2, [fields2], ...]
for i in range(2, len(raw_result), 2):
    row_data = raw_result[i]
    row = dict(zip(row_data[::2], row_data[1::2]))  # row_data is None -> TypeError

The same unguarded pattern is in every reply branch — sync and async, across WITHSCORES / standard / FT.AGGREGATE (≈ lines 256, 271, 276 and the async mirror ≈ 382, 397, 402 on main, v0.6.2). Any of them crashes if its row_data is nil.

Reproduction

The crash is deterministic given the reply shape — a FT.SEARCH reply where one document's field-array is None instead of a [field, value, …] list:

# Mirrors sql_redis/executor.py, standard FT.SEARCH branch.
#   reply layout: [count, key1, <fields1>, key2, <fields2>, ...]
#   here <fields1> came back nil:
raw_result = [1, "doc:1", None]

for i in range(2, len(raw_result), 2):
    row_data = raw_result[i]                        # None
    row = dict(zip(row_data[::2], row_data[1::2]))  # TypeError: 'NoneType' object is not subscriptable

How we hit it

We use sql-redis in redis-vl-python (a Redis vector/search library), whose integration tests run SQL SELECTs through Executor.execute against several Redis server versions. The crash appears:

  • Only against the redis:latest Docker image (currently Redis 8.8.0); never on pinned redis:8.2 / 8.4.
  • Across redis-py 5.x / 6.x / 7.x.
  • On different SELECT queries from run to run — i.e. data-independent, which points at the reply shape rather than any specific query.

Confirmed vs. unconfirmed

  • Confirmed: the parser isn't defensive against a nil field-array — the proximate cause of the exception.
  • Unconfirmed: why a slot comes back nil. We could not reproduce it directly, even with matched versions (Redis 8.8.0, redis-py 7.4.0, sql-redis 0.6.x) and high concurrency. Leading guesses: a RESP2/RESP3 reply-shape difference negotiated with the newer server/client, or a document expiring between FT.SEARCH returning ids and field materialization. We are not claiming a Redis 8.8.0 regression — it doesn't reproduce on 8.8.0.

Suggested fix

Tolerate a nil/empty field-array at each parse site, turning an intermittent hard crash into a well-defined result regardless of the underlying trigger:

row_data = raw_result[i]
if not row_data:
    continue  # or rows.append({})
row = dict(zip(row_data[::2], row_data[1::2]))

If the nil turns out to originate from a RESP3 reply layout, that path may warrant dedicated handling — happy to help probe HELLO 3 against 8.8.0.

Environment

  • sql-redis 0.6.0–0.6.2 (pattern present on main)
  • redis-py 5.x / 6.x / 7.x (observed on 7.4.0)
  • Redis server: 8.8.0 (redis:latest); not seen on 8.2 / 8.4
  • Python 3.11–3.14

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions