gastownhall/beads · error · issueops.ErrValidation

%w: delete id at position %d is blank

Error message

%w: delete id at position %d is blank

What it means

ValidateDeleteRequest wraps issueops.ErrValidation when an id at a specific position in the IDs slice is blank (whitespace-only after trimming). Blank entries would silently target nothing, so the validator reports the exact zero-based position that is bad.

Source

Thrown at internal/workapi/delete.go:39

// ValidateDeleteRequest applies the request rules every Deleter implementation
// shares, before anything is read.
//
// There is deliberately no require-a-filter analog of the sweep gate here: a
// delete request carries no predicate at all, so a caller cannot spell
// "everything" without typing every id. The guard that does matter — dependents
// outside the request — needs the graph and therefore lives in the bodies.
//
// The ExpectedVersion arity rule is here rather than in the bodies for the
// reason the rest of this file exists: it needs no database, and a rule the
// bodies each spelled themselves is a rule that can differ per backend.
func ValidateDeleteRequest(in issueops.DeleteRequest) error {
	if len(in.IDs) == 0 {
		return fmt.Errorf("%w: delete requires at least one issue id", issueops.ErrValidation)
	}
	for i, id := range in.IDs {
		if strings.TrimSpace(id) == "" {
			return fmt.Errorf("%w: delete id at position %d is blank", issueops.ErrValidation, i)
		}
	}
	// DISTINCT ids, not mentions: DeleteRequest.IDs promises duplicates
	// collapse, so an IDs of {"a", "a"} carrying a version names ONE row and is
	// legal. Counting the raw slice here would refuse the request the role's own
	// normalization rule says is fine.
	if in.ExpectedVersion != nil {
		if distinct := len(NormalizeDeleteIDs(in.IDs)); distinct > 1 {
			return fmt.Errorf("%w: expected-version delete names %d issues; one row version cannot describe more than one row",
				issueops.ErrValidation, distinct)
		}
	}
	return nil
}

// NormalizeDeleteIDs collapses duplicates, keeping the caller's FIRST mention
// of each id, and trims surrounding whitespace.
//

View on GitHub (pinned to 71377f2769)

Solutions

  1. Filter blank entries before calling delete: keep only ids where strings.TrimSpace(id) != ""
  2. Fix the upstream parsing so empty segments are dropped or rejected
  3. Use workapi.NormalizeDeleteIDs to collapse and clean the id list first

Example fix

// before
ids := strings.Split(input, ",")
req := issueops.DeleteRequest{IDs: ids}
// after
var ids []string
for _, s := range strings.Split(input, ",") {
    if v := strings.TrimSpace(s); v != "" { ids = append(ids, v) }
}
req := issueops.DeleteRequest{IDs: ids}
Defensive patterns

Strategy: validation

Validate before calling

cleaned := workapi.NormalizeDeleteIDs(req.IDs)
for i, id := range cleaned {
    if strings.TrimSpace(id) == "" { return fmt.Errorf("blank id at %d", i) }
}
req.IDs = cleaned

Try / catch

if err := api.Delete(ctx, req); err != nil {
    if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "is blank") {
        req.IDs = workapi.NormalizeDeleteIDs(req.IDs)
        return api.Delete(ctx, req)
    }
    return err
}

Prevention

When it happens

Trigger: Calling delete with IDs like ["bd-1", " ", "bd-3"]; splitting a comma-separated string that contains empty segments (e.g. "bd-1,,bd-3").

Common situations: strings.Split on user input producing empty fields; id fields sourced from sparse config or malformed JSON arrays; copy-paste artifacts.

Related errors


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