Skip to content

Online requests with zero entity rows return HTTP 500 instead of an empty result or a 400 #6819

Description

@Daksha1611

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions