Online requests with zero entity rows return HTTP 500 instead of an empty result or a 400
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 rangeAnd 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:169and: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_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 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_featuresreturning an empty frame for a zero-rowentity_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.
Source: feast-dev/feast