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
- Filter blank entries before calling delete: keep only ids where strings.TrimSpace(id) != ""
- Fix the upstream parsing so empty segments are dropped or rejected
- 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
- Split delimited id input with strings.Fields or filter empty segments
- Run NormalizeDeleteIDs on all externally sourced id lists
- Validate ids at parse time, not at API time
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
- %w: delete requires at least one issue id
- %w: expected-version delete names %d issues; one row version
- DeleteIssue: id must not be empty
- %w: bootstrap requires a non-empty issue prefix
- %w: bootstrap requires a project id; this role does not mint
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/918eaba052d99ed8.
Report an issue: GitHub.