gastownhall/beads · error

invalid offset %d: a query offset must be zero or greater

Error message

invalid offset %d: a query offset must be zero or greater

What it means

BuildQueryPlan rejects a negative offset with this message; offsets must be zero or greater. Like the limit check, it runs before query parsing and wraps issueops.ErrValidation for errors.Is classification. A negative offset has no meaning in row pagination, so it is refused rather than clamped.

Source

Thrown at internal/workapi/query.go:72

// Filter gets NO limit — the predicate must see every candidate row, or the
// page it produces is an arbitrary prefix of the answer reported as the whole
// of it (issueops/querier.go:118-133). The front doors used to bound that query
// at max(3*Limit, 100) and filter what came back; that bound is gone.
//
// It reads no configuration and touches no store, so both implementations share
// it without supplying a config source.
func BuildQueryPlan(in issueops.QueryRequest) (QueryPlan, error) {
	expression := strings.TrimSpace(in.Expression)
	if expression == "" {
		return QueryPlan{}, invalidQueryExpression("an expression is required")
	}
	limit := LimitOr(in.Limit, DefaultQueryLimit)
	if limit < 0 {
		return QueryPlan{}, fmt.Errorf("invalid limit %d: a query limit must be zero or greater; 0 means unlimited%.0w",
			limit, issueops.ErrValidation)
	}
	if in.Offset < 0 {
		return QueryPlan{}, fmt.Errorf("invalid offset %d: a query offset must be zero or greater%.0w",
			in.Offset, issueops.ErrValidation)
	}
	if in.Offset > 0 && in.SortBy != "" {
		return QueryPlan{}, fmt.Errorf(
			"invalid offset: an offset cannot be combined with a display order, because the order is applied to the rows the query bounded and each page would be sorted for itself%.0w",
			issueops.ErrValidation)
	}

	node, err := query.Parse(expression)
	if err != nil {
		return QueryPlan{}, invalidQueryExpression(err.Error())
	}
	result, err := query.NewEvaluator(time.Now()).Evaluate(node)
	if err != nil {
		return QueryPlan{}, invalidQueryExpression(err.Error())
	}

	plan := QueryPlan{

View on GitHub (pinned to 71377f2769)

Solutions

  1. Compute offset as max(0, (page-1)*pageSize) before building the request
  2. Use 0 for the first page and omit Offset when not paginating
  3. Treat 'unset' as 0 or a nil pointer, not -1

Example fix

// before
offset := (page - 1) * pageSize // page=0 → offset=-20
// after
if page < 1 { page = 1 }
offset := (page - 1) * pageSize
Defensive patterns

Strategy: validation

Validate before calling

if offset < 0 {
	return errors.New("offset must be >= 0")
}

Try / catch

plan, err := BuildQueryPlan(req)
if err != nil && errors.Is(err, issueops.ErrValidation) {
	// refuse the request; do not retry
}

Prevention

When it happens

Trigger: Calling BuildQueryPlan with issueops.QueryRequest.Offset < 0, typically from computed pagination (page-1)*pageSize when page was 0 or negative, or a client sending offset=-20.

Common situations: Page-number arithmetic bugs in API clients; an 'unset' sentinel of -1 leaking into the request; deserializing a JSON body where offset was omitted and a default of -1 applied.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/d480a9bf790ed361. Report an issue: GitHub.