Skip to content

fix(router): reject an empty reference id instead of matching every one - #718

Draft
vishal-bala wants to merge 1 commit into
mainfrom
fix/router-empty-reference-id-guard
Draft

fix(router): reject an empty reference id instead of matching every one#718
vishal-bala wants to merge 1 commit into
mainfrom
fix/router-empty-reference-id-guard

Conversation

@vishal-bala

Copy link
Copy Markdown
Collaborator

Motivation

SemanticRouter.delete_route_references(reference_ids=[""]) deletes a reference the caller never named. _make_filter_queries builds Tag("reference_id") == id with no guard on id, and Tag renders any falsy value as the match-all *. Both callers then read the first row of each query's results, so a match-all resolves to one arbitrary reference: get_route_references returns it, and delete_route_references removes it.

Reproduced against Redis 8.4 with a four-reference router:

--- get_route_references(reference_ids=['']) ---
returned 1 reference(s) for an id nobody named: ['hello']

--- delete_route_references(reference_ids=['']) ---
reported deleted: 1
keys removed: ['repro-router:greetings:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824']

The blast radius is one reference per empty id rather than the whole route, because each query carries LIMIT 0 10 and the caller takes only r[0]. It is still a silent deletion of data the caller did not ask about, and the reported count of 1 makes it look like the requested delete succeeded.

Changes

An empty reference id is rejected rather than matched

_make_filter_queries raises ValueError on a falsy id before constructing the expression. Rejecting rather than skipping is deliberate: an empty id in the list means the caller's own id-building went wrong, and quietly dropping it would hide that while still reporting success for the rest of the batch.

Both public methods already raise ValueError for unusable arguments, so the failure mode is unchanged in kind. The guard sits at the shared chokepoint, which covers get_route_references and delete_route_references together, including the path that derives ids by splitting stored keys.

Secondary changes

  • tests/unit/test_semantic_router_queries.py, a new hermetic file: _make_filter_queries is a static method that touches neither Redis nor a vectorizer, so a regression this consequential should be caught by a test that always runs rather than one behind the router's integration fixtures.
  • An integration test asserting that both calls raise and that the keyspace is byte-for-byte unchanged afterwards.

Notes

Whitespace-only ids are deliberately left alone. Tag("reference_id") == " " renders @reference_id:{\ \ }, which matches nothing rather than everything, so it already fails safe. Widening the guard to cover it would change behaviour without fixing a defect.

The guard was mutation-checked: removing it alone fails the three new unit cases and the integration test, and nothing else.

Release Notes

An empty reference id no longer resolves to a match-all filter. SemanticRouter.get_route_references and delete_route_references now raise ValueError when passed one, instead of matching every reference in the index.

This changes behaviour for callers that pass an empty id, which previously succeeded and now raises. Such a call was always operating on the wrong data: an empty id rendered as a match-all filter, so it resolved to one arbitrary reference. get_route_references returned a reference that was never requested, and delete_route_references deleted one and reported success. Code that swallowed the old result now sees an exception instead, which is the point.

Calls that pass real reference ids are unaffected, as are whitespace-only ids, which have always matched nothing rather than everything.

Next Steps

  1. Verify the router suites:
uv run pytest tests/unit/test_semantic_router_queries.py tests/integration/test_semantic_router.py -q

_make_filter_queries built `Tag("reference_id") == id` with no guard, and
Tag renders any falsy value as the match-all `*`. Both callers read the
first row of each query's results, so an empty id resolved to one
arbitrary reference: get_route_references returned a reference nobody
asked for, and delete_route_references deleted one and reported success.

Reproduced on Redis 8.4 against a four-reference router --
delete_route_references(reference_ids=[""]) removed a real reference key
and returned 1.

Raise ValueError at the shared chokepoint, which covers both public
methods and the path that derives ids by splitting stored keys.
Whitespace-only ids are left alone: they render a clause that matches
nothing, so they already fail safe.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant