{"record":{"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/6a6b581b48225afa0b76912d1028c6035baee932/redis/commands/core.py#L7672-L7708","documentation":"Raised by Redis.xnack() when retrycount is provided and is < 0. Note: there is NO isinstance check here -- only `if retrycount < 0:`. So a non-int that supports comparison may slip through to the server; only a negative comparable value raises. retrycount overrides the mode's implicit delivery-counter adjustment.","triggerScenarios":"Calling r.xnack('mystream', 'grp', 'FAIL', '1-0', retrycount=-1). A float like -0.5 also raises (< 0). A non-comparable retrycount would raise TypeError, not DataError.","commonSituations":"Computing retrycount as delivered - max_retries and underflowing below zero.","solutions":["Pass a non-negative int for retrycount, or omit it.","Clamp: retrycount = max(0, int(retrycount))."],"exampleFix":"# before\nr.xnack('mystream', 'grp', 'FAIL', '1-0', retrycount=delivered - maxr)  # negative\n\n# after\nr.xnack('mystream', 'grp', 'FAIL', '1-0', retrycount=max(0, delivered - maxr))","handlingStrategy":"validation","validationCode":"if retrycount is not None:\n    retrycount = int(retrycount)\n    if retrycount < 0:\n        raise ValueError('retrycount must be >= 0')","typeGuard":"lambda v: v is None or (isinstance(v, int) and not isinstance(v, bool) and v >= 0)","tryCatchPattern":"from redis.exceptions import DataError\ntry:\n    r.xnack('mystream', 'grp', 'FAIL', *ids, retrycount=rc)\nexcept DataError as e:\n    log.warning('bad xnack retrycount %r: %s', rc, e)","preventionTips":["Clamp retrycount to >= 0; note the guard is value-only, so coerce type yourself."],"tags":["stream","xnack","validation"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}