Expected Behavior
An online request carrying zero entity rows should be answered, not crash. A caller
whose upstream query matched nothing this run sends an empty batch, and a request that
genuinely omits a required join key should come back as a client error (4xx) naming the
key.
Current Behavior
Three related empty-input shapes surface internal exceptions, and the feature server
turns each into an HTTP 500 with an opaque message:
| Request |
Result |
store.get_online_features(features=[...], entity_rows=[]) |
IndexError: list index out of range |
POST /get-online-features with {"entities": {"driver_id": []}} |
HTTP 500 — "Missing join key values for keys: []. No values provided for keys: ['driver_id']. Provided join_key_values: ['driver_id']" |
POST /get-online-features with {"entities": {}} |
HTTP 500 — "'pop from an empty set'" |
The middle one is the worst of the three: the join key was supplied, it just has no
values, so this is a well-formed request that should return an empty result.
The same request shape with a single entity row returns 200.
Steps to reproduce
Against a local file + sqlite repo:
fs.get_online_features(features=["driver_stats:conv_rate"], entity_rows=[]).to_dict()
# IndexError: list index out of range
And through the server:
client = TestClient(get_app(fs), raise_server_exceptions=False)
client.post("/get-online-features", json={
"features": ["driver_stats:conv_rate"],
"entities": {"driver_id": []},
})
# 500 "Missing join key values for keys: []. No values provided for keys: ['driver_id']..."
client.post("/get-online-features", json={
"features": ["driver_stats:conv_rate"],
"entities": {},
})
# 500 "'pop from an empty set'"
Specifications
- Version:
master @ 5ad5592390febfca60c9d88edf7daccbdd156fd6
- Platform: Linux x86_64, Python 3.11.15
- Subsystem: online serving / feature server
Possible Solution
Three distinct causes:
sdk/python/feast/infra/online_stores/online_store.py:169 and :453 (the sync and
async paths) both build the columnar dict with {k: [] for k in entity_rows[0].keys()},
with no guard for an empty list.
sdk/python/feast/utils.py:554 _validate_entity_values ends with
set_of_row_lengths.pop(). For an empty mapping the set is empty and .pop() raises
KeyError: 'pop from an empty set'.
sdk/python/feast/utils.py _get_unique_entities treats "join key present but holding
zero values" the same as "join key never supplied". The row-wise conversion immediately
below it already handles zero rows (if not rowise: return (), (), 0), so the guard is
rejecting a case the code beneath it supports.
A reasonable contract, and the one I've implemented in the linked PR:
- join keys supplied but empty → empty response with the correct feature-name metadata,
consistent with get_historical_features returning an empty frame for a zero-row
entity_df;
- nothing supplied at all → a typed client error carrying HTTP 400 rather than a 500.
I went with an empty response rather than a 400 for the empty-but-present case because
it keeps batch-scoring callers from having to special-case "my filter matched nothing".
Happy to switch it to a 400 if maintainers prefer the stricter reading — it is a one-line
change either way.
Expected Behavior
An online request carrying zero entity rows should be answered, not crash. A caller
whose upstream query matched nothing this run sends an empty batch, and a request that
genuinely omits a required join key should come back as a client error (4xx) naming the
key.
Current Behavior
Three related empty-input shapes surface internal exceptions, and the feature server
turns each into an HTTP 500 with an opaque message:
store.get_online_features(features=[...], entity_rows=[])IndexError: list index out of rangePOST /get-online-featureswith{"entities": {"driver_id": []}}"Missing join key values for keys: []. No values provided for keys: ['driver_id']. Provided join_key_values: ['driver_id']"POST /get-online-featureswith{"entities": {}}"'pop from an empty set'"The middle one is the worst of the three: the join key was supplied, it just has no
values, so this is a well-formed request that should return an empty result.
The same request shape with a single entity row returns 200.
Steps to reproduce
Against a local
file+sqliterepo:And through the server:
Specifications
master@5ad5592390febfca60c9d88edf7daccbdd156fd6Possible Solution
Three distinct causes:
sdk/python/feast/infra/online_stores/online_store.py:169and:453(the sync andasync paths) both build the columnar dict with
{k: [] for k in entity_rows[0].keys()},with no guard for an empty list.
sdk/python/feast/utils.py:554_validate_entity_valuesends withset_of_row_lengths.pop(). For an empty mapping the set is empty and.pop()raisesKeyError: 'pop from an empty set'.sdk/python/feast/utils.py_get_unique_entitiestreats "join key present but holdingzero values" the same as "join key never supplied". The row-wise conversion immediately
below it already handles zero rows (
if not rowise: return (), (), 0), so the guard isrejecting a case the code beneath it supports.
A reasonable contract, and the one I've implemented in the linked PR:
consistent with
get_historical_featuresreturning an empty frame for a zero-rowentity_df;I went with an empty response rather than a 400 for the empty-but-present case because
it keeps batch-scoring callers from having to special-case "my filter matched nothing".
Happy to switch it to a 400 if maintainers prefer the stricter reading — it is a one-line
change either way.