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 := true

View on GitHub (pinned to 909b515798)

Solutions

  1. List current decisions (cscli decisions list -a) and use a valid, still-active ID
  2. Handle errors.Is(err, database.DeleteFail) as a not-found condition rather than retrying
  3. 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

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


AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06). Data as JSON: /api/errors/1910abd29985580f. Report an issue: GitHub.