redis/redis-py · error · DataError

Category "{encoder.decode(category, force=True)}" must be pr

Error message

Category "{encoder.decode(category, force=True)}" must be prefixed with "+" or "-"

What it means

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.

Source

Thrown at redis/commands/core.py:534

                    )

        if nopass:
            pieces.append(b"nopass")

        if categories:
            for category in categories:
                category = encoder.encode(category)
                # categories can be prefixed with one of (+@, +, -@, -)
                if category.startswith(b"+@"):
                    pieces.append(category)
                elif category.startswith(b"+"):
                    pieces.append(b"+@%s" % category[1:])
                elif category.startswith(b"-@"):
                    pieces.append(category)
                elif category.startswith(b"-"):
                    pieces.append(b"-@%s" % category[1:])
                else:
                    raise DataError(
                        f'Category "{encoder.decode(category, force=True)}" '
                        'must be prefixed with "+" or "-"'
                    )
        if commands:
            for cmd in commands:
                cmd = encoder.encode(cmd)
                if not cmd.startswith(b"+") and not cmd.startswith(b"-"):
                    raise DataError(
                        f'Command "{encoder.decode(cmd, force=True)}" '
                        'must be prefixed with "+" or "-"'
                    )
                pieces.append(cmd)

        if keys:
            for key in keys:
                key = encoder.encode(key)
                if not key.startswith(b"%") and not key.startswith(b"~"):
                    key = b"~%s" % key

View on GitHub (pinned to 6a6b581b48)

Solutions

  1. Prefix every categories entry with '+' (grant) or '-' (revoke), e.g. '+@read' or '-@dangerous'.
  2. Use the shorthand '+read' which the client rewrites to '+@read' internally.
  3. Validate prefixes in your config layer before building the call.

Example fix

# before
client.acl_setuser('alice', categories=['@read', '@write'])
# after
client.acl_setuser('alice', categories=['+@read', '+@write'])
Defensive patterns

Strategy: validation

Validate before calling

def normalize_categories(categories):
    out = []
    for c in categories:
        if not (c.startswith('+') or c.startswith('-')):
            c = '+' + c  # default to grant
        out.append(c)
    return out

def safe_acl_setuser_categories(client, username, categories):
    return client.acl_setuser(username, categories=normalize_categories(categories))

Type guard

def is_prefixed_category(c) -> bool:
    return isinstance(c, str) and len(c) > 1 and c[0] in '+-'

Try / catch

from redis.exceptions import DataError
try:
    client.acl_setuser('alice', categories=categories)
except DataError as e:
    if 'must be prefixed' in str(e):
        categories = ['+' + c if not c[:1] in '+-' else c for c in categories]
        client.acl_setuser('alice', categories=categories)
    else:
        raise

Prevention

When it happens

Trigger: Calling client.acl_setuser('alice', categories=['@read']) (leading '@' without '+'/'-'), categories=['read', '+write'] (some unprefixed), or categories=['=read']. The error message echoes the offending category.

Common situations: Assuming the client wraps bare category names with '+@'; passing the redis-cli form '@read' verbatim; mixing prefixed and unprefixed entries.

Related errors


AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10). Data as JSON: /api/errors/bda48e8dc4081ae3. Report an issue: GitHub.