8000 fix(api): enforce ES's per-index search request limits with ES's messages by amartya1523 · Pull Request #1859 · xerj-org/xerj · GitHub
[go: up one dir, main page]

Skip to content

fix(api): enforce ES's per-index search request limits with ES's messages - #1859

Merged
xerj-org merged 3 commits into
xerj-org:mainfrom
amartya1523:fix/search-request-limits
Oct 20, 2026
Merged

xerj-org merged 3 commits into
xerj-org:mainfrom
amartya1523:fix/search-request-limits

Conversation

@amartya1523
Copy link
Copy Markdown
Contributor

AI-assisted change, run and reviewed by @amartya1523.

Burns down all 8 search/30_limits.yml lines of the catch-lenient.txt ratchet from #1583 (151 → 143). Independent of #1824 and #1854: different code (search_impl and the search-body struct, not the aggregation validators), but the same catch-lenient.txt, so the second to merge needs a trivial rebase there.

What this changes, and why

Each case expects the 400 Elasticsearch 8.13 returns before running a search. XERJ either answered 200 or refused with a different message:

Case Before After (ES's message)
?from=-1 Failed to parse int parameter [from] with value [-1] [from] parameter cannot be negative but was [-1] (illegal_argument_exception)
{"from": -1} serde type error (expected usize) same reason, from a deserializer that refuses negatives
?from=10000 (size 10) engine's own wording Result window is too large, from + size must be less than or equal to: [10000] but was [10010]. See the scroll api …
?scroll=5m&size=10010 200: the size was rewritten to the snapshot cap before any check Batch size is too large, size must be less than or equal to: [10000] but was [10010]. …
rescore window_size: 10001 200 Rescore window [10001] is too large. It must be less than [10000]. …
3 docvalue_fields vs max_docvalue_fields_search: 2 200 Trying to retrieve too many docvalue_fields. Must be less than or equal to: [2] but was [3]. …
regexp / /…/ query_string of 1110 chars 200 or an unrelated error The length of regex [1110] used in the Regexp Query request has exceeded the allowed maximum of [1000]. …
prefix of 1110 chars 200 The length of prefix [1110] used in the Prefix Query request has exceeded …

Fix (es_compat.rs):

  • Negative from: a URL from that parses as a negative integer gets ES's illegal_argument_exception. The body's from uses deserialize_non_negative_from, which refuses a negative value with the same reason.
  • find_index_request_limit_violation:
    • It runs per participating index right after the existing max_script_fields check, so it stays after the ids-vs-window check that 630_ids_query.yml relies on.
    • It uses the caller's from/size, captured before the scroll rewrite.
    • Settings come from index_int_setting, a generalisation of index_max_result_window's three-spelling lookup. The defaults are ES's: rescore window = result window, 100 docvalue_fields, 1000-character regex.
    • The regex and prefix check walks the whole query (bool clauses included).
  • The engine's own result-window check is unchanged and stays as a backstop. Every internal es_compat::search caller (graph, memory) either uses small sizes or would already be refused by that engine check, so only the message changes for them.

Evidence

Full ES-YAML suite against a node built from this commit. The runner reported exactly the 8 30_limits.yml entries as stale, and 0 failed. With them deleted and enforced:

$ es-yaml-runner --dir tests/es-compat-yaml/yaml
1402 passed · 0 failed · 10 skipped · 1412 total
143 cases ran catch-lenient (an expected error XERJ does not raise yet; see catch-lenient.txt)

The other limit cases in that file (Script_fields size limit) and 630_ids_query.yml still pass.

Checks

  • cargo fmt --all -- --check (exit 0)
  • Scoped release build: cargo build --release -p xerj-server -p es-yaml-runner
  • ES-YAML suite at 0 failed (above). The node ran with [limits] disk_flood_stage_percent = 0 because this machine's disk is above the 95% flood stage.
  • Conformance coverage: the 8 existing upstream 30_limits.yml cases, now strict
  • No new unit tests. The eight upstream YAML cases are the regression tests, run against the real binary.
  • Docs: none describe the old behaviour

Not run: cargo test -p xerj-api (release/fat-LTO test binaries do not fit this machine's free disk; CI runs them) and clippy.

Scope note: these checks are in the _search path (search_impl). _msearch and the template endpoints build their requests separately and are not covered here. No YAML case exercises them, and they keep the engine's window check.

Provenance

  • Written by: AI-assisted, run by @amartya1523, who has reviewed it.
  • Verified (commands run, output observed): everything under Evidence and Checks, on macOS aarch64, rustc 1.99.1.
  • Assumed (not tested): ES's exact wording for the query_string regex case. The YAML only pins The length of regex [1110], and I reuse the Regexp-query sentence.

…ages

Motivation:
All eight `search/30_limits.yml` cases in `catch-lenient.txt` (xerj-org#1583)
expect a 400 that XERJ did not return, or returned with a different
message: a negative `from` (URL or body), `from + size` over
`index.max_result_window`, a scroll batch over it, a rescore window over
`index.max_rescore_window`, more `docvalue_fields` than
`index.max_docvalue_fields_search`, and a `regexp` / `prefix` /
`/regex/` query_string longer than `index.max_regex_length`.

Root cause:
- `?from=-1` failed the `usize` parse and got the generic "Failed to
  parse int parameter"; `{"from": -1}` failed serde with a type error.
- The result window was only checked inside the engine, with its own
  message; a scroll request had its size rewritten to the snapshot cap
  first, so it was never checked at all.
- Rescore window, docvalue_fields count and regex/prefix length were
  never checked (HTTP 200).

Fix (es_compat.rs):
- `?from` that parses as a negative integer -> ES's
  `illegal_argument_exception` `[from] parameter cannot be negative but
  was [-1]`; the body's `from` uses a deserializer that refuses a
  negative value with the same reason.
- `find_index_request_limit_violation`, run per participating index next
  to the existing `max_script_fields` check, using the caller's
  `from`/`size` captured before the scroll rewrite, with ES 8.13's
  messages for: result window / scroll batch size, rescore window
  (defaulting to the result window, as ES does), docvalue_fields count
  (default 100), and regex / prefix length (default 1000, walking the
  whole query). Settings are read by a new `index_int_setting`, the
  generalisation of `index_max_result_window`'s three-spelling lookup.
The engine's own window check is unchanged and stays as a backstop.

Evidence:
- Full ES-YAML suite against a node built from this commit: 0 failed,
  and the runner reported exactly the eight 30_limits entries stale; with
  them deleted and enforced: `1402 passed · 0 failed · 10 skipped`,
  catch-lenient 151 -> 143.
@cla-bot
cla-bot Bot commented Oct 19, 2026
Copy link
Copy Markdown

Thanks for this contribution — we'd like to merge it.

Before we can, we need a signed Contributor License Agreement from: @amartya1523

It is a one-time thing per contributor, not per pull request. Read CLA.md, then open a small pull request adding your GitHub username to .contributors — that pull request is your signature, since it is made from your own account.

Once it is merged, comment @cla-bot check here and this check will go green. If you believe you are already covered, comment @cla-bot check and it will re-verify.

@xerj-org
Copy link
Copy Markdown
Owner

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label Oct 20, 2026
@cla-bot
cla-bot Bot commented Oct 20, 2026
Copy link
Copy Markdown

Re-checking the CLA status for every commit author on this pull request…

The non-negative `from` deserializer read an i64, so from=u64::MAX failed
as a JSON type error instead of ES's 'Result window is too large'
(rerank_stage_http every_refusal_is_a_400_that_names_the_problem_and_sends_nothing).
Accept u64 too, saturating to usize::MAX; negatives still get ES's message.
@xerj-org
Copy link
Copy Markdown
Owner

Pushed a small fix (7d5434b) on top: the new non-negative from deserializer read an i64, so a huge positive from (u64::MAX) failed as a JSON type error before reaching the result-window check, which broke rerank_stage_http::every_refusal_is_a_400_that_names_the_problem_and_sends_nothing. It now accepts u64 as well (saturating), and negatives still get ES's [from] parameter cannot be negative message. Locally: that test file 54/54, xerj-api lib 358/358, clippy and fmt clean. Will merge once Build + Test is green.

@xerj-org
xerj-org merged commit 27b2f6f into xerj-org:main Oct 20, 2026
22 of 23 checks passed
@xerj-org
Copy link
Copy Markdown
Owner

Xerj Merger made the fix: reviewed the full diff: ES's per-index search limits (result window and scroll batch, rescore window, docvalue_fields, regex and prefix length, negative from) are now enforced with ES's messages, and the 8 30_limits catch-lenient lines are gone. Fix commit 7d5434b lets a huge positive body from reach the window check, so the rerank refusal test passes. Note for a follow-up: find_overlong_pattern walks every key, so a field literally named prefix or regexp with a value over 1000 characters inside another query could be refused by mistake. verified locally at 425dc3fdae (main 764e7a9 + all six batch-B PRs; fmt, clippy -D warnings, full workspace test compile, all lib unit tests and xerj-api integration tests) and at 9a466a4871 (main 5585819 + the same PRs; full cargo test --profile ci-test --workspace, 5676 passed, 0 failed) ahead of the CI backlog on owner's instruction.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0