gastownhall/beads · warning

%w: close batch requires at least one item

Error message

%w: close batch requires at least one item

What it means

ValidateCloseBatchRequest rejects a CloseBatchRequest with zero items, wrapping storage.ErrValidation. A batch close with no items is a no-op misuse rather than something a backend should silently succeed on, so the shared contract rejects it up front. Callers should match with errors.Is(err, storage.ErrValidation).

Source

Thrown at internal/storage/issueops/close_batch.go:23

	"database/sql"
	"fmt"
	"strings"

	"github.com/steveyegge/beads/internal/storage"
	"github.com/steveyegge/beads/internal/types"
	publicops "github.com/steveyegge/beads/issueops"
)

// ValidateCloseBatchRequest applies the request rules every BatchCloser
// implementation shares, so a rule is a contract rather than one backend's
// habit. It rejects the request outright; a per-item refusal is a result, not
// a validation failure, and never reaches here.
func ValidateCloseBatchRequest(request publicops.CloseBatchRequest) error {
	if request.Actor == "" {
		return fmt.Errorf("%w: close batch requires an actor", storage.ErrValidation)
	}
	if len(request.Items) == 0 {
		return fmt.Errorf("%w: close batch requires at least one item", storage.ErrValidation)
	}
	for i, item := range request.Items {
		if item.IssueID == "" {
			return fmt.Errorf("%w: close batch item %d requires an issue ID", storage.ErrValidation, i)
		}
	}
	if request.ClaimNext != nil {
		if err := ValidateClaimNextRequest(publicops.ClaimNextRequest{Actor: request.Actor, Filter: *request.ClaimNext}); err != nil {
			return err
		}
	}
	return nil
}

// CloseBatchCommitMessage is the history entry a batch records. It is the
// only spelling — the request carries no label to override it, and could not
// compose this one, because it names what LANDED rather than what was asked for,
// which is why it is composed from the result and not from the request: a

View on GitHub (pinned to 71377f2769)

Solutions

  1. Guard before calling: check len(request.Items) > 0 and skip or report 'nothing to close' instead of issuing the call.
  2. Match errors.Is(err, storage.ErrValidation) and return a friendly 'no issues selected' message.
  3. Fix upstream selection logic that produced an empty candidate list unexpectedly.
  4. In API layers, return 400-style validation output before touching storage.

Example fix

// before
err := closer.CloseBatch(ctx, publicops.CloseBatchRequest{Actor: actor, Items: nil})
// after
if len(items) == 0 {
    return fmt.Errorf("no issues to close")
}
err := closer.CloseBatch(ctx, publicops.CloseBatchRequest{Actor: actor, Items: items})
Defensive patterns

Strategy: validation

Validate before calling

if len(items) == 0 {
    return fmt.Errorf("no issues selected for batch close")
}

Type guard

func hasBatchItems(req publicops.CloseBatchRequest) bool {
    return len(req.Items) > 0
}

Try / catch

if err := ValidateCloseBatchRequest(req); err != nil {
    if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "at least one item") {
        return ErrNothingToClose // typed sentinel for callers
    }
    return err
}

Prevention

When it happens

Trigger: Passing CloseBatchRequest with a nil or empty Items slice to any BatchCloser implementation that invokes ValidateCloseBatchRequest.

Common situations: Upstream filtering (e.g. filtering a list of issues by status) accidentally removed every candidate before batching; loops that append to the wrong slice; JSON payloads where items was omitted or empty; tests with unpopulated fixtures.

Related errors


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