You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Chained-condition error paths: second &= leg unparseable, |= chain → 500 with wrong message, parseQuery TypeError without query arg #2435
Three related error-path defects around REST chained conditions (&= / |=). None produces silent wrong data (unlike the chained-value coercion bug filed separately) — these fail loudly but wrongly.
1. A second chained leg can never be parsed
skiLengths=ge=170&=le=180&=ge=171 fails to parse with the unrelated error attribute must be specified before equality comparator.
Cause: after each token the parser picks its tokenizer by parser = attribute ? VALUE_PARSER : QUERY_PARSER (resources/search.ts:1571-1574). A chained leg sets attribute = '' (falsy), so parsing continues with QUERY_PARSER, whose operator class cannot produce &= as one token (VALUE_PARSER's [&|=]* run can — that is the only reason the first&= works, since at that point attribute is the real attribute name and truthy). The split & + = then trips the equality-comparator error.
Consequence: the planner's Multiple chained conditions are not currently supported error (resources/Table.ts:3530) is unreachable via REST, and the user gets a misleading message for a syntactically plausible query.
2. |= chains throw a plain 500 with the wrong message
skiLengths=ge=175|=le=180 parses fine (one chained condition, operator: 'or'), then prepareConditions throws Error('Multiple chained conditions are not currently supported') (resources/Table.ts:3530) — a plain Error (→ 500), and the message is wrong twice over: there is only one chained condition, and the unsupported part is the or operator. Should be a 4xx ClientError saying or-chaining is unsupported.
3. parseQuery(str) without a query object crashes on any parse error
parseQuery reads query.parseErrorMessage where query may be undefined — resources/search.ts:1343:
should read currentQuery.parseErrorMessage. With no query argument, any recorded parse error becomes TypeError: Cannot read properties of undefined (reading 'parseErrorMessage') instead of the intended SyntaxViolation. Production REST always passes a query object (RequestTarget.ts:105), so this only hits direct callers of the public Resource.parseQuery(search) static and internal tooling.
Reproductions
All three reproduce with parseQuery + Table.search on main (a21910c); pinning tests are in the linked verification PR.
Severity
Error-path/UX only: loud failures, wrong shape (misleading message, 500 instead of 400, TypeError instead of SyntaxViolation). No wrong result sets.
Independent re-verification of this area turned up a fourth variant, and this one does produce silent wrong data (unlike the three above): a chain whose first leg is a non-range comparator but whose second leg is a valid lt/le is not rejected — it is silently collapsed into an exclusive lower bound, discarding the stated comparator.
The collapse (resources/Table.ts:3517-3545 on main @ a21910c) validates only the upper leg (must be lt/le/gt/ge check) and classifies anything that isn't gt/ge as the lower leg without validating it; isGe then defaults every unrecognized first-leg comparator (eq, sw, ct, …) to exclusivegt.
Repro (unit level, same route as the reproductions above):
sizes: { elements: { type: 'Int' } }, records: 1→[4], 2→[5], 3→[6]
sizes=eq=4&=le=5 → explain: gtle [4,"5"] → returns [2,3]
the one record whose element equals 4 is EXCLUDED;
6 is included (the #2433 string-bound no-op compounds it)
The pure-string case shows it with no coercion involvement at all:
name: String, records: 'exact-four', 'five', 'six'
name=eq=five&=le=z → gtle ("five","z"] → returns only 'six'
the record that MATCHES the stated equality is excluded
(it sits exactly on the now-exclusive lower bound)
Expected: reject the chain (only gt/ge + lt/le pairs are meaningful) with a 4xx, like the existing rejection of a non-range second leg (tags=sw=pre&=ew=ale throws). Actual: silent reinterpretation, wrong result set on both indexed and unindexed paths.
Found while independently verifying the element-scoping questions this issue's PR pins — happy for it to be folded into this issue or split out.
— Claude Fable (dispatch harper-array-elem-scoping-verify)
Activity
kriszyp commentedon Sep 1, 2026
Independent re-verification of this area turned up a fourth variant, and this one does produce silent wrong data (unlike the three above): a chain whose first leg is a non-range comparator but whose second leg is a valid
lt/leis not rejected — it is silently collapsed into an exclusive lower bound, discarding the stated comparator.The collapse (
resources/Table.ts:3517-3545onmain@ a21910c) validates only the upper leg (must be lt/le/gt/gecheck) and classifies anything that isn'tgt/geas the lower leg without validating it;isGethen defaults every unrecognized first-leg comparator (eq,sw,ct, …) to exclusivegt.Repro (unit level, same route as the reproductions above):
The pure-string case shows it with no coercion involvement at all:
Expected: reject the chain (only
gt/ge+lt/lepairs are meaningful) with a 4xx, like the existing rejection of a non-range second leg (tags=sw=pre&=ew=alethrows). Actual: silent reinterpretation, wrong result set on both indexed and unindexed paths.Found while independently verifying the element-scoping questions this issue's PR pins — happy for it to be folded into this issue or split out.
— Claude Fable (dispatch
harper-array-elem-scoping-verify)Pin element-scoping semantics of queries over array-valued properties (…