🐛 fix(api): reject unknown /api/v1/search params and alias separators - #80
Conversation
GET /api/v1/search accepted any unrecognized query parameter, returned 200, and applied nothing. A caller who misspelled an exclusion filter got back exactly the content they meant to exclude with a success code and no signal — a filter failing OPEN, indistinguishable in the response and access log from a correct query. The trap is easy to hit because filters take hyphens (not-path) while scoring params take underscores (half_life), so the wrong separator is the natural mistake. Canonicalize every incoming key to a closed accepted set before reading any value: an unknown key is now a 400 (fail closed), with a did-you-mean when it is one edit away from a real one. Both separators are accepted as aliases for every parameter and merged into the same dimension, so a caller cannot guess wrong and hyphen/underscore variants OR together. Value-validation 400s keep their exact wording and now fire through the alias spelling with the canonical name in the message. Strictness is confined to /search; /stats and /graph are untouched (§4.1 filters unchanged in meaning — only the HTTP key surface changes). Closes #77
cwest
left a comment
There was a problem hiding this comment.
No changes needed.
The key handling is sound: canonicalization runs before any value is read, an unknown key fails closed with a 400 rather than the old silent 200, and both separators resolve to the same canonical name so a caller can't guess wrong. The did-you-mean bound scales with key length, so a near miss like notpath suggests not-path while nonsense_param gets a bare 400. Canonicalization is confined to the search handler; /stats and /graph keep their own closures and never touch it, so strictness can't leak.
Ran the full suite under -race (8 packages green), the conformance suite green, and reproduced every acceptance case against the real 262-node corpus at this head:
notpath/exclude_path/nonsense_paramnow 400;notpathnamesnot-pathin the suggestion.- The disclosure path is closed:
not_path=caseyis now a valid alias returning 0casey/nodes, byte-identical tonot-path=casey(both3b5d4e07), not the old 200-with-2-leaked-nodes. min-relevance=0.30is honoured via the alias: hits 12 to 5, byte-identical tomin_relevance=0.30(both85535cb7).- The four value-validation 400s keep their exact wording, and an alias like
half-life=abcreports the canonicalhalf_lifename. ?type=empty value, repeatedpath,/stats?bogus=1, and/graph?not_a_param=xall stay 200 — no regression, no leak.
The alias set lands both separators for every parameter, so the sibling lexical-gate work inherits canonicalization without having to pick a convention.
Closes #77.
What
GET /api/v1/searchaccepted any unrecognized query parameter, returnedHTTP 200, and applied nothing. A caller who misspelled an exclusion filter
got back exactly the content they meant to exclude, with a success status and
no signal — a filter failing open, indistinguishable in the response and
access log from a correct query. The trap is structural: filters take hyphens
(
not-path) while scoring params take underscores (half_life), so the wrongseparator is the natural mistake and there is no rule a caller can infer.
This change, confined to
internal/apiserver/search.goplus tests:did-you-mean when the key is one edit away from a real one:
unknown query parameter "notpath"; did you mean "not-path"?hyphen/underscore variants into the same dimension, so a caller cannot
guess wrong and repeated aliases OR together.
Value-validation 400s keep their exact wording and now also fire through the
alias spelling with the canonical name in the message. Strictness is
confined to
/search;/statsand/graphare untouched. No spec change —OKF v0.2 §4.1 filters are unchanged in meaning; only the HTTP key surface's
handling changes.
Conformance gate (run, not assumed)
Real corpus (the layer fixtures cannot substitute for)
Bundle
~/src/knowledge-base/bundles/knowledge, 262 nodes, hash embedder(
hash-test-embedder), served on a copy. BEFORE = binary at78ae7c3,AFTER = this branch. Base query
?q=agent+orchestration+workflow&k=12,baseline body sha256[0:16] =
2c6cb99cb829cd58.not-path=casey5d466ac15d466ac1not_path=casey2c6cb99c5d466ac1not-pathnotpath=caseydid you mean "not-path"?exclude_path=caseynonsense_param=1not_tag=research2c6cb99c40b285b2not-tag=researchmin-relevance=0.30c3f900bdmin_relevance=0.30min_relevance=0.30c3f900bdc3f900bdhalf-life=72c6cb99c2c6cb99c(==half_life=7)The card's sharpest control, separator alone deciding whether the filter fires:
not_path=caseydisclosure closed: the one-character typo returned 2casey/nodes at 200 BEFORE; AFTER it is honoured as an alias and returns 0
casey/nodes, byte-for-byte identical to
not-path=casey.Value-validation 400s — byte-identical wording BEFORE→AFTER (controls)
Strictness does not leak off /search (control)
validate + lint --strict on the real corpus (controls, HTTP-only change)
Both controls proven (per detector change)
notpath/exclude_path/nonsense_param→ 400;not_path/min-relevance/not_tagaliases now honoured with reordered/filtered output.
documented spelling still 200;
?type=empty reads as unset; repeated paramsstill OR; value-validation 400 wording unchanged;
/statsand/graphuntouched. The unit test
TestSearch_HyphenAliasHonouredForScoringParamusesthe dated decay fixture at
half_life=90so the honoured-with-reorder path isproven where the top-12 real-corpus rows do not reorder at
hl=7.Ordering note (#78)
The alias set lands both separators for every parameter, so the sibling
lexical-gate card can name its parameter in either convention and inherit the
canonicalization for free.
§-citations
OKF v0.2 SPEC.md verified upstream (
Version 0.2); §4.1 is Frontmatter —the filters key on
type/tag/path metadata defined there and are unchangedin meaning.