crowdsecurity/crowdsec · warning · DeleteFail
decision with id '%d' doesn't exist: %w
Error message
decision with id '%d' doesn't exist: %w
What it means
ExpireDecisionByID first queries the decision by ID; if the query errors or returns zero rows, it treats both cases as 'decision does not exist' and wraps the DeleteFail sentinel (the code even contains an unreachable second check for ItemNotFound, intentionally returning 404-like semantics per the XXX comment).
Source
Thrown at pkg/database/decisions.go:370
rows, err := c.deleteDecisionBatch(ctx, batch)
if err != nil {
return err
}
total += rows
return nil
})
return total, err
}
// ExpireDecisionByID set the expiration of a decision to now()
func (c *Client) ExpireDecisionByID(ctx context.Context, decisionID int) (int, []*ent.Decision, error) {
toUpdate, err := c.Ent.Decision.Query().Where(decision.IDEQ(decisionID)).All(ctx)
// XXX: do we want 500 or 404 here?
if err != nil || len(toUpdate) == 0 {
c.Log.Warningf("ExpireDecisionByID : %v (nb expired: %d)", err, len(toUpdate))
return 0, nil, fmt.Errorf("decision with id '%d' doesn't exist: %w", decisionID, DeleteFail)
}
if len(toUpdate) == 0 {
return 0, nil, ItemNotFound
}
count, err := c.ExpireDecisions(ctx, toUpdate)
return count, toUpdate, err
}
func (c *Client) CountDecisionsByValue(ctx context.Context, value string, since *time.Time, onlyActive bool) (int, error) {
rng, err := csnet.NewRange(value)
if err != nil {
return 0, fmt.Errorf("unable to convert '%s' to int: %w", value, err)
}
contains := trueView on GitHub (pinned to 909b515798)
Solutions
- List current decisions (cscli decisions list -a) and use a valid, still-active ID
- Handle errors.Is(err, database.DeleteFail) as a not-found condition rather than retrying
- Re-fetch the ID immediately before expiry to avoid stale-ID races
Example fix
// before
client.ExpireDecisionByID(ctx, staleID) // ID may already be gone
// after
decs, err := client.QueryDecisionsWithFilter(ctx, ...)
for _, d := range decs {
if _, _, err := client.ExpireDecisionByID(ctx, d.ID); err != nil {
if !errors.Is(err, database.DeleteFail) {
return err
}
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// confirm the ID exists before expiring
found, err := entClient.Decision.Query().Where(decision.IDEQ(id)).Count(ctx)
if err != nil || found == 0 { return fmt.Errorf("decision %d not found", id) } Try / catch
if _, _, err := client.ExpireDecisionByID(ctx, id); err != nil {
if errors.Is(err, database.DeleteFail) {
log.Printf("decision %d already gone", id) // treat as not-found
return nil
}
return err
} Prevention
- Treat DeleteFail from this call as not-found, not as a server fault
- Re-list decisions right before deletion to avoid stale IDs
- Expect races: another operator may have removed the decision first
When it happens
Trigger: Calling ExpireDecisionByID with a decisionID that is not in the database — the decision already expired and was deleted, the ID is wrong, or the query itself failed.
Common situations: cscli decisions delete --id 42 after the decision already expired; scripts caching IDs from a previous listing; race where another operator removed the decision first.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
- object not found
- ErrFeatureUnknown
- while creating machine entry for %s: %w
- while selecting machine entry for %s: %w
- allowlist %s not found
AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06).
Data as JSON: /api/errors/1910abd29985580f.
Report an issue: GitHub.