{"id":"1a4363c8bafb59d4","repo":"redis/redis-py","slug":"xnack-retrycount-must-be-0","errorCode":null,"errorMessage":"XNACK retrycount must be >= 0","messagePattern":"XNACK retrycount must be >= 0","errorType":"validation","errorClass":"DataError","httpStatus":null,"severity":"error","filePath":"redis/commands/core.py","lineNumber":7690,"sourceCode":"                already in the group's PEL.\n\n        Returns:\n            The number of messages successfully NACKed.\n\n        For more information, see https://redis.io/commands/xnack\n        \"\"\"\n        if not ids:\n            raise DataError(\"XNACK requires at least one message ID\")\n\n        if mode not in {\"SILENT\", \"FAIL\", \"FATAL\"}:\n            raise DataError(\"XNACK mode must be one of: SILENT, FAIL, FATAL\")\n\n        pieces: list = [name, groupname, mode, \"IDS\", len(ids)]\n        pieces.extend(ids)\n\n        if retrycount is not None:\n            if retrycount < 0:\n                raise DataError(\"XNACK retrycount must be >= 0\")\n            pieces.extend([b\"RETRYCOUNT\", retrycount])\n\n        if force:\n            pieces.append(b\"FORCE\")\n\n        return self.execute_command(\"XNACK\", *pieces)\n\n    @overload\n    def xpending(\n        self: SyncClientProtocol, name: KeyT, groupname: GroupT\n    ) -> dict[str, Any]: ...\n\n    @overload\n    def xpending(\n        self: AsyncClientProtocol, name: KeyT, groupname: GroupT\n    ) -> Awaitable[dict[str, Any]]: ...\n\n    def xpending(","sourceCodeStart":7672,"sourceCodeEnd":7708,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/commands/core.py#L7672-L7708","documentation":"Raised by xnack() when optional `retrycount` is negative. CAUTION: unlike xclaim, there is NO isinstance check here - only `retrycount < 0` (core.py:7689). So a negative int raises DataError correctly, but a non-numeric retrycount (e.g. a string) makes `str < 0` raise an uncaught TypeError that crashes the call.","triggerScenarios":"xnack(..., retrycount=-1) (int) -> DataError. xnack(..., retrycount='-1') (str) -> TypeError, NOT this DataError. retrycount=None or >=0 is fine.","commonSituations":"Passing retrycount from JSON as a string; a decremented counter that went below zero; expecting the guard to also validate type.","solutions":["Pass a non-negative int, or None to let the mode decide.","Coerce and clamp: rc = None if raw is None else max(0, int(raw)).","Do NOT pass a string - it will crash with TypeError before this DataError can fire."],"exampleFix":"# before\nr.xnack('s','g','FAIL', *ids, retrycount=attempts)  # attempts == -1\n# after\nr.xnack('s','g','FAIL', *ids, retrycount=None if attempts is None else max(0, int(attempts)))","handlingStrategy":"validation","validationCode":"rc = None if retrycount is None else max(0, int(retrycount))\nr.xnack(name, group, mode, *ids, retrycount=rc)","typeGuard":"def opt_nonneg_int(v):\n    return None if v is None else max(0, int(v))","tryCatchPattern":"from redis.exceptions import DataError\ntry:\n    r.xnack(name, group, mode, *ids, retrycount=rc)\nexcept DataError:\n    r.xnack(name, group, mode, *ids, retrycount=max(0, int(rc)))","preventionTips":["Coerce retrycount to int yourself - the guard does not type-check.","Clamp negatives to 0 or pass None."],"tags":["redis","streams","xnack","validation","range-error","weak-guard"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}