Online requests with zero entity rows return HTTP 500 instead of an empty result or a 400
- Dominant language
- Python
- Stars
- 7.3k
- Forks
- 1.4k
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 15
Description
## 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:
```python
fs.get_online_features(features=["driver_stats:conv_rate"], entity_rows=[]).to_dict()
# IndexError: list index out of range
```
And through the server:
```python
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.
Contributor guide
Research direction
Start with the empty-input reproductions, then read sdk/python/feast/infra/online_stores/online_store.py at lines 169 and 453 and sdk/python/feast/utils.py around _validate_entity_values and _get_unique_entities. Done means empty supplied join-key values return an empty result with feature metadata, while no supplied keys produces a typed HTTP 400 naming the missing key.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend-api-design, data-engineering
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100