{"record":{"id":"1910abd29985580f","repo":"crowdsecurity/crowdsec","slug":"decision-with-id-d-doesn-t-exist-w","errorCode":null,"errorMessage":"decision with id '%d' doesn't exist: %w","messagePattern":"decision with id '(.+?)' doesn't exist: %w","errorType":"exception","errorClass":"DeleteFail","httpStatus":null,"severity":"warning","filePath":"pkg/database/decisions.go","lineNumber":370,"sourceCode":"\t\trows, err := c.deleteDecisionBatch(ctx, batch)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttotal += rows\n\t\treturn nil\n\t})\n\n\treturn total, err\n}\n\n// ExpireDecisionByID set the expiration of a decision to now()\nfunc (c *Client) ExpireDecisionByID(ctx context.Context, decisionID int) (int, []*ent.Decision, error) {\n\ttoUpdate, err := c.Ent.Decision.Query().Where(decision.IDEQ(decisionID)).All(ctx)\n\n\t// XXX: do we want 500 or 404 here?\n\tif err != nil || len(toUpdate) == 0 {\n\t\tc.Log.Warningf(\"ExpireDecisionByID : %v (nb expired: %d)\", err, len(toUpdate))\n\t\treturn 0, nil, fmt.Errorf(\"decision with id '%d' doesn't exist: %w\", decisionID, DeleteFail)\n\t}\n\n\tif len(toUpdate) == 0 {\n\t\treturn 0, nil, ItemNotFound\n\t}\n\n\tcount, err := c.ExpireDecisions(ctx, toUpdate)\n\n\treturn count, toUpdate, err\n}\n\nfunc (c *Client) CountDecisionsByValue(ctx context.Context, value string, since *time.Time, onlyActive bool) (int, error) {\n\trng, err := csnet.NewRange(value)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"unable to convert '%s' to int: %w\", value, err)\n\t}\n\n\tcontains := true","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/database/decisions.go#L352-L388","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nclient.ExpireDecisionByID(ctx, staleID) // ID may already be gone\n// after\ndecs, err := client.QueryDecisionsWithFilter(ctx, ...)\nfor _, d := range decs {\n    if _, _, err := client.ExpireDecisionByID(ctx, d.ID); err != nil {\n        if !errors.Is(err, database.DeleteFail) {\n            return err\n        }\n    }\n}","handlingStrategy":"try-catch","validationCode":"// confirm the ID exists before expiring\nfound, err := entClient.Decision.Query().Where(decision.IDEQ(id)).Count(ctx)\nif err != nil || found == 0 { return fmt.Errorf(\"decision %d not found\", id) }","typeGuard":null,"tryCatchPattern":"if _, _, err := client.ExpireDecisionByID(ctx, id); err != nil {\n    if errors.Is(err, database.DeleteFail) {\n        log.Printf(\"decision %d already gone\", id) // treat as not-found\n        return nil\n    }\n    return err\n}","preventionTips":["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"],"tags":["go","not-found","database","id"],"backgroundTag":"record-not-found","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}