{"record":{"id":"bda48e8dc4081ae3","repo":"redis/redis-py","slug":"category-encoder-decode-category-force-true","errorCode":null,"errorMessage":"Category \"{encoder.decode(category, force=True)}\" must be prefixed with \"+\" or \"-\"","messagePattern":"Category \"(.+?)\" must be prefixed with \"\\+\" or \"-\"","errorType":"exception","errorClass":"DataError","httpStatus":null,"severity":"error","filePath":"redis/commands/core.py","lineNumber":534,"sourceCode":"                    )\n\n        if nopass:\n            pieces.append(b\"nopass\")\n\n        if categories:\n            for category in categories:\n                category = encoder.encode(category)\n                # categories can be prefixed with one of (+@, +, -@, -)\n                if category.startswith(b\"+@\"):\n                    pieces.append(category)\n                elif category.startswith(b\"+\"):\n                    pieces.append(b\"+@%s\" % category[1:])\n                elif category.startswith(b\"-@\"):\n                    pieces.append(category)\n                elif category.startswith(b\"-\"):\n                    pieces.append(b\"-@%s\" % category[1:])\n                else:\n                    raise DataError(\n                        f'Category \"{encoder.decode(category, force=True)}\" '\n                        'must be prefixed with \"+\" or \"-\"'\n                    )\n        if commands:\n            for cmd in commands:\n                cmd = encoder.encode(cmd)\n                if not cmd.startswith(b\"+\") and not cmd.startswith(b\"-\"):\n                    raise DataError(\n                        f'Command \"{encoder.decode(cmd, force=True)}\" '\n                        'must be prefixed with \"+\" or \"-\"'\n                    )\n                pieces.append(cmd)\n\n        if keys:\n            for key in keys:\n                key = encoder.encode(key)\n                if not key.startswith(b\"%\") and not key.startswith(b\"~\"):\n                    key = b\"~%s\" % key","sourceCodeStart":516,"sourceCodeEnd":552,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/commands/core.py#L516-L552","documentation":"Raised by Redis.acl_setuser() when an entry in the `categories` list does not begin with '+' or '-'. Each ACL category permission must be prefixed to indicate grant or revoke. The library accepts forms like '+@category', '-@category', '+category' (rewritten to '+@category'), or '-category'; any entry lacking a leading '+'/'-' is rejected. The decoded (human-readable) category value is interpolated into the message.","triggerScenarios":"Calling client.acl_setuser('alice', categories=['@read']) (leading '@' without '+'/'-'), categories=['read', '+write'] (some unprefixed), or categories=['=read']. The error message echoes the offending category.","commonSituations":"Assuming the client wraps bare category names with '+@'; passing the redis-cli form '@read' verbatim; mixing prefixed and unprefixed entries.","solutions":["Prefix every categories entry with '+' (grant) or '-' (revoke), e.g. '+@read' or '-@dangerous'.","Use the shorthand '+read' which the client rewrites to '+@read' internally.","Validate prefixes in your config layer before building the call."],"exampleFix":"# before\nclient.acl_setuser('alice', categories=['@read', '@write'])\n# after\nclient.acl_setuser('alice', categories=['+@read', '+@write'])","handlingStrategy":"validation","validationCode":"def normalize_categories(categories):\n    out = []\n    for c in categories:\n        if not (c.startswith('+') or c.startswith('-')):\n            c = '+' + c  # default to grant\n        out.append(c)\n    return out\n\ndef safe_acl_setuser_categories(client, username, categories):\n    return client.acl_setuser(username, categories=normalize_categories(categories))","typeGuard":"def is_prefixed_category(c) -> bool:\n    return isinstance(c, str) and len(c) > 1 and c[0] in '+-'","tryCatchPattern":"from redis.exceptions import DataError\ntry:\n    client.acl_setuser('alice', categories=categories)\nexcept DataError as e:\n    if 'must be prefixed' in str(e):\n        categories = ['+' + c if not c[:1] in '+-' else c for c in categories]\n        client.acl_setuser('alice', categories=categories)\n    else:\n        raise","preventionTips":["Use '+@name' / '-@name' consistently for category permissions.","Wrap acl_setuser in a helper that enforces prefixes for categories, commands, and selectors.","Validate ACL config files at load time before applying."],"tags":["acl","validation","acl-setuser","categories","input-validation","prefix"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}