#3031·tantivy

QueryParser::parse_query panics on `- *` instead of returning Err (Exist query without a field)

Author: coconutbirdCreated Aug 5, 2026Updated Aug 5, 2026

Describe the bug

QueryParser::parse_query panics on some malformed queries instead of returning Err(QueryParserError). The panic is an Option::expect in the query grammar, reached from an ordinary user-supplied query string.

Minimal input: "- *" — an occur operator, whitespace, then *.

thread 'main' panicked at tantivy-query-grammar-0.22.0/src/user_input_ast.rs:43:30:
Exist query without a field isn't allowed

Which version are you using?

  • tantivy 0.22.1
  • tantivy-query-grammar 0.22.0 (currently the latest published version)

To Reproduce

rust
use tantivy::schema::{Schema, TEXT};
use tantivy::{doc, Index};

fn main() {
    let mut b = Schema::builder();
    let body = b.add_text_field("body", TEXT);
    let index = Index::create_in_ram(b.build());
    {
        let mut w = index.writer(15_000_000).unwrap();
        w.add_document(doc!(body => "hello world")).unwrap();
        w.commit().unwrap();
    }

    let parser = tantivy::query::QueryParser::for_index(&index, vec![body]);
    let _ = parser.parse_query("- *"); // panics
}

tantivy_query_grammar::parse_query("- *") panics on its own too, so this does not depend on the schema or the default-field configuration.

Expected behavior

Err(QueryParserError), as for any other malformed query. parse_query returns a Result, so callers reasonably assume malformed input is reported through it rather than by unwinding.

Observed behaviour across nearby inputs

Whitespace between the operator and * is what separates a panic from a clean error:

Input Result
- * panic
+ * panic
a - * panic
- *( panic
-* Err(QueryParserError)
-*( Err(QueryParserError)
* - Err(QueryParserError)
*, a:*, :*, (*), NOT *, *:* parse fine

Cause

tantivy-query-grammar-0.22.0/src/user_input_ast.rs:42-44:

rust
UserInputLeaf::Exists { field: _ } => UserInputLeaf::Exists {
    field: field.expect("Exist query without a field isn't allowed"),
},

set_field is called during parsing (query_grammar.rs:338, and the lenient equivalent at :384) with the Option<String> produced by opt(field_name). When the leaf is Exists and that option is None, the expect fires. Returning a parse error there instead would keep the failure inside the existing Result contract.

Impact

Services that accept query strings from callers inherit a remote panic. It is worse for binaries built with panic = "abort", where there is no unwinding to contain it and catch_unwind is not an available mitigation — a single malformed query terminates the process.

We hit this in production: one query string aborted a search node and took every tenant on it down until the pod restarted. Happy to supply more detail if useful.

Note

The hazardous shapes are not obvious. An exhaustive search over a:*() "[] up to length 5 found nothing, and hand-written "obvious" hostile strings (*, a:*, :*, (*), NOT *, *:*) all parse cleanly. That makes it awkward for downstream users to guard against by input validation, which is part of why we are reporting it rather than filtering locally.