gastownhall/beads · error
invalid query expression: %s
Error message
invalid query expression: %s
What it means
invalidQueryExpression is the single refusal shape for any expression that fails — an empty expression, or one query.Parse cannot parse — always formatted as 'invalid query expression: <detail>' and wrapping issueops.ErrValidation. The prefix is load-bearing: internal/httpapi maps it back to the `q` parameter so a bad expression surfaces as the documented 400 rather than a 500.
Source
Thrown at internal/workapi/query.go:131
// backend, and the probe row is genuinely last.
plan.Filter.Limit = limit
plan.Filter.SortBy = in.SortBy
plan.Filter.SortDesc = in.Reverse
}
// The default closed exclusion, applied only to an expression that has no
// opinion of its own about status (issueops/querier.go:29-41).
if !in.IncludeClosed && plan.Filter.Status == nil && !mentionsStatus(node) {
plan.Filter.ExcludeStatus = append(plan.Filter.ExcludeStatus, types.StatusClosed)
}
return plan, nil
}
// invalidQueryExpression is the one refusal shape a bad expression takes. The
// prefix is load-bearing: internal/httpapi maps it back to the `q` parameter
// so an unparseable expression is the documented 400 rather than a 500.
func invalidQueryExpression(detail string) error {
return fmt.Errorf("invalid query expression: %s%.0w", detail, issueops.ErrValidation)
}
// mentionsStatus reports whether the expression compares `status` anywhere.
// An expression that does keeps its own answer about closed rows; one that
// does not gets the default exclusion.
func mentionsStatus(node query.Node) bool {
switch n := node.(type) {
case *query.ComparisonNode:
return n.Field == "status"
case *query.AndNode:
return mentionsStatus(n.Left) || mentionsStatus(n.Right)
case *query.OrNode:
return mentionsStatus(n.Left) || mentionsStatus(n.Right)
case *query.NotNode:
return mentionsStatus(n.Operand)
default:
return false
}View on GitHub (pinned to 71377f2769)
Solutions
- Read the detail after the prefix — it names the parse failure or 'an expression is required'
- Check errors.Is(err, issueops.ErrValidation) to confirm it is a client-input 400, not a server fault
- Validate the expression syntax against the query language docs (comparison operators, quoted values)
- Guard against empty/whitespace input before calling: strings.TrimSpace(expr) != ""
Example fix
// before bd query 'status = ' // after bd query 'status = open'
Defensive patterns
Strategy: try-catch
Validate before calling
expr := strings.TrimSpace(userExpr)
if expr == "" {
return errors.New("query expression is required")
} Try / catch
plan, err := BuildQueryPlan(req)
var verr error
if err != nil {
if errors.Is(err, issueops.ErrValidation) && strings.HasPrefix(err.Error(), "invalid query expression") {
// documented 400 path: map detail back to the q parameter
}
return err
} Prevention
- Trim and require a non-empty expression before calling
- Test user-supplied expressions with query.Parse before shipping them
- Quote values with spaces; use the documented operators only
- Re-validate saved expressions after field renames
When it happens
Trigger: Calling BuildQueryPlan with in.Expression empty after trimming, or containing syntax query.Parse rejects — unbalanced quotes/parens, unknown operators, malformed comparisons like `status =`.
Common situations: Users typing ad-hoc `bd query 'status == open'` with wrong operator syntax; templates interpolating an empty variable into the expression; quoting issues in shells stripping inner quotes; schema changes renaming a field an old saved expression references.
Related errors
- unknown field: %s
- invalid status: %s
- priority must be between 0 and 4
- invalid boolean value: %s
- expected value at position %d, got %s
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/3369fb9e6b857730.
Report an issue: GitHub.