From f4e7f4d273aa1da98de266fe5d41d4713f8a9d96 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Tue, 3 Nov 2026 14:02:08 +0000 Subject: [PATCH 01/11] DOC-7253 Show the redis-py and Jedis exceptions on the common error pages Adds data/client-exceptions.json, an error-clients shortcode (with a Markdown/JSON expansion), and an "In client libraries" table on each of the 27 pages in errors/common. The table replaces the 15 hand-written "In redis-py, this error is" sentences. Specific class names also go into each page's match list so the lookup box finds a pasted exception name. The data is keyed by page file name, not by server prefix or message. The pages already group the errors a reader sees, and the same prefix can map to different classes depending on the message (redis-py maps ERR variants one by one). Per-message data would be 1,783 rows of mostly the generic class. Both clients were run live, with the same scenarios as build/errors/live_checks.py. redis-py comes from live_baseline.json (7.4.1). For Jedis 8.0.2 I copied live_checks.py and swapped the redis-py client for a small Java probe that runs each command through Jedis and prints the exception class and message. All 25 rows read from Protocol.processError matched the run. DENIED and max-clients, which I could not predict from source because the server closes the socket, are JedisConnectionException with "Unexpected end of stream." in Jedis. The probe and its harness are not in the repo yet. The two clients differ in a way that matters for the tables. redis-py strips the prefix from mapped errors (ReadOnlyError says "You can't write..."), but leaves it on unmapped ones (ResponseError says "WRONGTYPE ..."). Jedis always keeps the full message and maps only MOVED, ASK, CLUSTERDOWN, BUSY, NOSCRIPT, NOAUTH, WRONGPASS, NOPERM and NOPROTO. In Jedis, CLUSTERDOWN is a JedisClusterException that extends JedisDataException, and redis-py MovedError extends AskError. markdown-error-clients.html runs on every page that goes through process-markdown-content.html, including generated sections such as /commands that have no .File. Reading .File there broke the whole Hugo build, so it now looks for the shortcode first and guards .File. Learned: redis-py and Jedis map server prefixes to classes very differently, so the table is per page and shows the message each client produces, not just the class Constraint: layouts/partials/markdown-error-clients.html must not read .Page.File unless the page contains the shortcode, because generated pages have no File and the build fails Directive: Add a client to data/client-exceptions.json by running it against the live_checks.py scenarios first, and add its id to "order"; do not write rows from the client's docs Directive: Do not add the generic classes (ResponseError, JedisDataException, ConnectionError) to a page's match list, because they would match every pasted error Gaps: The JedisCluster and RedisCluster notes on MOVED and ASK (they follow the redirect themselves) come from source, not from a live run Gaps: Jedis was run against server 8.10.2 and redis-py against 8.10.3 Recheck: when a Jedis release changes Protocol.processError or the exception hierarchy Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- .../develop/reference/errors/common/ask.md | 5 +- .../develop/reference/errors/common/busy.md | 5 + .../reference/errors/common/busygroup.md | 4 + .../reference/errors/common/busykey.md | 4 + .../reference/errors/common/clusterdown.md | 5 +- .../reference/errors/common/crossslot.md | 4 +- .../develop/reference/errors/common/denied.md | 4 + .../errors/common/err-max-clients.md | 4 +- .../errors/common/err-not-an-integer.md | 4 + .../errors/common/err-syntax-error.md | 4 + .../errors/common/err-unknown-command.md | 4 + .../common/err-wrong-number-of-arguments.md | 4 + .../reference/errors/common/execabort.md | 6 +- .../reference/errors/common/loading.md | 4 +- .../reference/errors/common/masterdown.md | 4 +- .../reference/errors/common/misconf.md | 4 + .../develop/reference/errors/common/moved.md | 5 +- .../develop/reference/errors/common/noauth.md | 8 +- .../reference/errors/common/nogroup.md | 4 + .../develop/reference/errors/common/noperm.md | 5 +- .../reference/errors/common/noreplicas.md | 4 + .../reference/errors/common/noscript.md | 5 +- .../develop/reference/errors/common/oom.md | 6 +- .../reference/errors/common/readonly.md | 6 +- .../reference/errors/common/tryagain.md | 6 +- .../reference/errors/common/wrongpass.md | 6 +- .../reference/errors/common/wrongtype.md | 4 + data/client-exceptions.json | 304 ++++++++++++++++++ layouts/partials/errors/client-rows.html | 26 ++ layouts/partials/markdown-error-clients.html | 35 ++ .../partials/process-markdown-content.html | 1 + layouts/shortcodes/error-clients.html | 30 ++ 32 files changed, 504 insertions(+), 20 deletions(-) create mode 100644 data/client-exceptions.json create mode 100644 layouts/partials/errors/client-rows.html create mode 100644 layouts/partials/markdown-error-clients.html create mode 100644 layouts/shortcodes/error-clients.html diff --git a/content/develop/reference/errors/common/ask.md b/content/develop/reference/errors/common/ask.md index c9b420e930..323f706ead 100644 --- a/content/develop/reference/errors/common/ask.md +++ b/content/develop/reference/errors/common/ask.md @@ -18,6 +18,7 @@ error: message: ASK : match: - AskError + - JedisAskDataException --- A Redis cluster returns this error while a hash slot is moving from one node to another, for example, while you reshard the cluster. The key you asked for isn't on the node you sent the command to, so it may already be on the new node. The error gives the slot number and the new node's address: @@ -26,7 +27,9 @@ A Redis cluster returns this error while a hash slot is moving from one node to ASK 8000 127.0.0.1:6461 ``` -In redis-py, this error is `AskError: 8000 127.0.0.1:6461`. +## In client libraries + +{{< error-clients >}} ## Example diff --git a/content/develop/reference/errors/common/busy.md b/content/develop/reference/errors/common/busy.md index b8dbb94fc1..fbd660757c 100644 --- a/content/develop/reference/errors/common/busy.md +++ b/content/develop/reference/errors/common/busy.md @@ -21,6 +21,7 @@ error: - Redis is busy running a module command - UNKILLABLE - Script killed by user + - JedisBusyException --- Redis returns this error when a Lua script or function has run for longer than the `busy-reply-threshold` setting, which is 5 seconds by default. While a script runs, Redis doesn't run any other command. So instead of making other clients wait, Redis answers almost every command with this error, even `PING`. @@ -31,6 +32,10 @@ BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN N For a function, the message names `FUNCTION KILL` instead of `SCRIPT KILL`. A module command that runs for a long time causes a similar error: `BUSY Redis is busy running a module command.` +## In client libraries + +{{< error-clients >}} + ## Example While another client runs a script that never ends: diff --git a/content/develop/reference/errors/common/busygroup.md b/content/develop/reference/errors/common/busygroup.md index 1809a99c2c..eefe3d1e00 100644 --- a/content/develop/reference/errors/common/busygroup.md +++ b/content/develop/reference/errors/common/busygroup.md @@ -26,6 +26,10 @@ Redis returns this error from [`XGROUP CREATE`](/content/commands/xgroup-create. BUSYGROUP Consumer Group name already exists ``` +## In client libraries + +{{< error-clients >}} + ## Example ``` diff --git a/content/develop/reference/errors/common/busykey.md b/content/develop/reference/errors/common/busykey.md index 835cb243fb..25259358dc 100644 --- a/content/develop/reference/errors/common/busykey.md +++ b/content/develop/reference/errors/common/busykey.md @@ -26,6 +26,10 @@ Redis returns this error when [`RESTORE`](/content/commands/restore.md) would cr BUSYKEY Target key name already exists. ``` +## In client libraries + +{{< error-clients >}} + ## Example ``` diff --git a/content/develop/reference/errors/common/clusterdown.md b/content/develop/reference/errors/common/clusterdown.md index 10144d51e6..7c547181a4 100644 --- a/content/develop/reference/errors/common/clusterdown.md +++ b/content/develop/reference/errors/common/clusterdown.md @@ -20,6 +20,7 @@ error: - Hash slot not served - The cluster is down - ClusterDownError + - JedisClusterException --- A Redis cluster returns this error when it can't serve your command. The message has two versions: @@ -32,7 +33,9 @@ CLUSTERDOWN The cluster is down - `Hash slot not served` means that no node serves the key's hash slot. This happens when some slots were never assigned to a node, for example, in a cluster you haven't finished setting up. - `The cluster is down` means that the cluster is in a failed state. By default, a cluster stops serving commands when any hash slot has no working node. For example, this happens when a primary node fails and has no replica to take its place. -In redis-py, this error is `ClusterDownError: The cluster is down` or `ClusterDownError: Hash slot not served`. +## In client libraries + +{{< error-clients >}} ## Example diff --git a/content/develop/reference/errors/common/crossslot.md b/content/develop/reference/errors/common/crossslot.md index 13f618dcc4..f511bac0fa 100644 --- a/content/develop/reference/errors/common/crossslot.md +++ b/content/develop/reference/errors/common/crossslot.md @@ -27,7 +27,9 @@ A Redis cluster returns this error when one command uses keys that belong to dif CROSSSLOT Keys in request don't hash to the same slot ``` -In redis-py, this error is `ClusterCrossSlotError: Keys in request don't hash to the same slot`. +## In client libraries + +{{< error-clients >}} ## Example diff --git a/content/develop/reference/errors/common/denied.md b/content/develop/reference/errors/common/denied.md index 8b2cec21de..0c4eb2f878 100644 --- a/content/develop/reference/errors/common/denied.md +++ b/content/develop/reference/errors/common/denied.md @@ -28,6 +28,10 @@ DENIED Redis is running in protected mode because protected mode is enabled and Some client libraries don't show this message. For example, redis-py only reports `ConnectionError: Connection closed by server.` +## In client libraries + +{{< error-clients >}} + ## How to fix it Protected mode keeps a server that has no password away from the network. The safest fix is to set a password. Protected mode then lets other computers connect, as long as they authenticate: diff --git a/content/develop/reference/errors/common/err-max-clients.md b/content/develop/reference/errors/common/err-max-clients.md index 18f40c8900..dd713921a4 100644 --- a/content/develop/reference/errors/common/err-max-clients.md +++ b/content/develop/reference/errors/common/err-max-clients.md @@ -28,7 +28,9 @@ ERR max number of clients reached In a cluster, the connections from other cluster nodes count toward the limit too, and the message is `ERR max number of clients + cluster connections reached`. -In redis-py, this error is `ConnectionError: max number of clients reached`. +## In client libraries + +{{< error-clients >}} ## How to fix it diff --git a/content/develop/reference/errors/common/err-not-an-integer.md b/content/develop/reference/errors/common/err-not-an-integer.md index bd48b78cef..7a9a00d02a 100644 --- a/content/develop/reference/errors/common/err-not-an-integer.md +++ b/content/develop/reference/errors/common/err-not-an-integer.md @@ -28,6 +28,10 @@ Redis returns this error when a command needs a whole number but gets something ERR value is not an integer or out of range ``` +## In client libraries + +{{< error-clients >}} + ## Example [`INCR`](/content/commands/incr.md) adds 1 to the number stored in a key, so it fails when the key holds text: diff --git a/content/develop/reference/errors/common/err-syntax-error.md b/content/develop/reference/errors/common/err-syntax-error.md index 5f72609ee9..ab6c430424 100644 --- a/content/develop/reference/errors/common/err-syntax-error.md +++ b/content/develop/reference/errors/common/err-syntax-error.md @@ -26,6 +26,10 @@ Redis returns this error when it can't make sense of a command's options. The co ERR syntax error ``` +## In client libraries + +{{< error-clients >}} + ## Example The `NX` and `XX` options of [`SET`](/content/commands/set.md) can't be used together: diff --git a/content/develop/reference/errors/common/err-unknown-command.md b/content/develop/reference/errors/common/err-unknown-command.md index f8d0500edc..1b0713b179 100644 --- a/content/develop/reference/errors/common/err-unknown-command.md +++ b/content/develop/reference/errors/common/err-unknown-command.md @@ -27,6 +27,10 @@ Redis returns this error when it doesn't recognize the command you sent. The mes ERR unknown command 'FOO', with args beginning with: 'bar' ``` +## In client libraries + +{{< error-clients >}} + ## Example ``` diff --git a/content/develop/reference/errors/common/err-wrong-number-of-arguments.md b/content/develop/reference/errors/common/err-wrong-number-of-arguments.md index fcbc84cf04..ce29e31dfe 100644 --- a/content/develop/reference/errors/common/err-wrong-number-of-arguments.md +++ b/content/develop/reference/errors/common/err-wrong-number-of-arguments.md @@ -26,6 +26,10 @@ Redis returns this error when a command has more or fewer arguments than it acce ERR wrong number of arguments for 'get' command ``` +## In client libraries + +{{< error-clients >}} + ## Example ``` diff --git a/content/develop/reference/errors/common/execabort.md b/content/develop/reference/errors/common/execabort.md index a6cfeda1c4..60aff1e0f5 100644 --- a/content/develop/reference/errors/common/execabort.md +++ b/content/develop/reference/errors/common/execabort.md @@ -28,6 +28,10 @@ Redis returns this error from [`EXEC`](/content/commands/exec.md) when a command EXECABORT Transaction discarded because of previous errors. ``` +## In client libraries + +{{< error-clients >}} + ## Example Between [`MULTI`](/content/commands/multi.md) and `EXEC`, Redis queues each command to run later. A command with an error, such as an unknown command name, is rejected instead of queued. `EXEC` then refuses to run the transaction: @@ -53,6 +57,4 @@ If Redis refuses the `EXEC` command itself, the message gives the reason after a EXECABORT Transaction discarded because of: NOPERM User has no permissions to run the 'exec' command ``` -In redis-py, a transaction pipeline raises the error of the command that failed, rather than `EXECABORT`. For example: `Command # 1 (FOO) of pipeline caused error: unknown command 'FOO'`. - Errors that happen while `EXEC` runs the commands don't discard the transaction. For example, a [`WRONGTYPE`](/content/develop/reference/errors/common/wrongtype.md) error makes only that command fail. The other commands still run, and `EXEC` returns the error in its list of replies. See [Errors inside a transaction](/content/develop/using-commands/transactions.md#errors-inside-a-transaction). diff --git a/content/develop/reference/errors/common/loading.md b/content/develop/reference/errors/common/loading.md index 536ad65a24..6edf8fa2e1 100644 --- a/content/develop/reference/errors/common/loading.md +++ b/content/develop/reference/errors/common/loading.md @@ -27,7 +27,9 @@ Redis returns this error while it's loading your data into memory. This happens LOADING Redis is loading the dataset in memory ``` -In redis-py, this error is `BusyLoadingError: Redis is loading the dataset in memory`. +## In client libraries + +{{< error-clients >}} ## Example diff --git a/content/develop/reference/errors/common/masterdown.md b/content/develop/reference/errors/common/masterdown.md index 7ca238ad21..e257ac8a1a 100644 --- a/content/develop/reference/errors/common/masterdown.md +++ b/content/develop/reference/errors/common/masterdown.md @@ -27,7 +27,9 @@ A replica returns this error when it has lost its connection to its primary, and MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. ``` -In redis-py, this error is `MasterDownError: Link with MASTER is down and replica-serve-stale-data is set to 'no'.` +## In client libraries + +{{< error-clients >}} ## How to fix it diff --git a/content/develop/reference/errors/common/misconf.md b/content/develop/reference/errors/common/misconf.md index 7939027f26..40bcbbdeda 100644 --- a/content/develop/reference/errors/common/misconf.md +++ b/content/develop/reference/errors/common/misconf.md @@ -36,6 +36,10 @@ Redis returns this error when it can't save your data to disk. So that you notic MISCONF Errors writing to the AOF file: No space left on device ``` +## In client libraries + +{{< error-clients >}} + ## How to fix it 1. Find out why saving failed. The Redis log gives the reason, for example: diff --git a/content/develop/reference/errors/common/moved.md b/content/develop/reference/errors/common/moved.md index 700c91b842..7285b1dec6 100644 --- a/content/develop/reference/errors/common/moved.md +++ b/content/develop/reference/errors/common/moved.md @@ -18,6 +18,7 @@ error: message: MOVED : match: - MovedError + - JedisMovedDataException --- A Redis cluster returns this error when you send a command to a node that doesn't serve the key. The error tells you which node does. A client that supports clusters reads the error, sends the command again to the right node, and updates its map of the cluster. @@ -28,7 +29,9 @@ A cluster splits keys into 16384 hash slots, and each primary node serves some o MOVED 12182 127.0.0.1:6463 ``` -In redis-py, this error is `MovedError: 12182 127.0.0.1:6463`. +## In client libraries + +{{< error-clients >}} ## Example diff --git a/content/develop/reference/errors/common/noauth.md b/content/develop/reference/errors/common/noauth.md index a441dd6567..3ef72a73e2 100644 --- a/content/develop/reference/errors/common/noauth.md +++ b/content/develop/reference/errors/common/noauth.md @@ -19,6 +19,8 @@ error: match: - Authentication required - HELLO must be called with the client already authenticated + - AuthenticationError + - JedisAccessControlException --- Redis returns this error when the server requires a password, but your connection hasn't sent one yet. Until the connection authenticates, the server refuses almost every command, even `PING`. @@ -27,10 +29,12 @@ Redis returns this error when the server requires a password, but your connectio NOAUTH Authentication required. ``` -In redis-py, this error is `AuthenticationError: Authentication required.` - If the first command is [`HELLO`](/content/commands/hello.md), the error also says how to authenticate in the same command: `NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time`. +## In client libraries + +{{< error-clients >}} + ## Example ``` diff --git a/content/develop/reference/errors/common/nogroup.md b/content/develop/reference/errors/common/nogroup.md index bd5a03bfd8..994e5c5217 100644 --- a/content/develop/reference/errors/common/nogroup.md +++ b/content/develop/reference/errors/common/nogroup.md @@ -28,6 +28,10 @@ Redis returns this error when a stream command uses a consumer group that doesn' NOGROUP No such key 'mystream' or consumer group 'mygroup' in XREADGROUP with GROUP option ``` +## In client libraries + +{{< error-clients >}} + ## Example ``` diff --git a/content/develop/reference/errors/common/noperm.md b/content/develop/reference/errors/common/noperm.md index cf93ed9a2d..2a0da8cb70 100644 --- a/content/develop/reference/errors/common/noperm.md +++ b/content/develop/reference/errors/common/noperm.md @@ -20,6 +20,7 @@ error: - has no permissions to run - No permissions to access - NoPermissionError + - JedisAccessControlException --- Redis returns this error when you're authenticated, but your user's access control list (ACL) rules don't allow what you asked for. The message says what was refused, a command or a key: @@ -29,7 +30,9 @@ NOPERM User app has no permissions to run the 'set' command NOPERM No permissions to access a key ``` -In redis-py, this error is `NoPermissionError`, followed by the message without the `NOPERM` prefix. +## In client libraries + +{{< error-clients >}} ## Example diff --git a/content/develop/reference/errors/common/noreplicas.md b/content/develop/reference/errors/common/noreplicas.md index ac6acc5cec..e57a5806f0 100644 --- a/content/develop/reference/errors/common/noreplicas.md +++ b/content/develop/reference/errors/common/noreplicas.md @@ -28,6 +28,10 @@ A replica only counts if it's up to date. Each replica reports to its primary ev NOREPLICAS Not enough good replicas to write. ``` +## In client libraries + +{{< error-clients >}} + ## How to fix it 1. On the primary, run [`INFO replication`](/content/commands/info.md). `connected_slaves` shows how many replicas are connected. Each `slave0`, `slave1`, and so on line shows a replica's address and its `lag` in seconds. diff --git a/content/develop/reference/errors/common/noscript.md b/content/develop/reference/errors/common/noscript.md index 1baa3dd9fe..835b8d15a5 100644 --- a/content/develop/reference/errors/common/noscript.md +++ b/content/develop/reference/errors/common/noscript.md @@ -19,6 +19,7 @@ error: match: - No matching script - NoScriptError + - JedisNoScriptException --- Redis returns this error when you run a Lua script with [`EVALSHA`](/content/commands/evalsha.md), but the server doesn't have the script in its script cache. `EVALSHA` runs a script by its SHA1 (Secure Hash Algorithm 1) digest, a short code calculated from the script's source. The server only knows the digest of a script that it has already loaded. @@ -27,7 +28,9 @@ Redis returns this error when you run a Lua script with [`EVALSHA`](/content/com NOSCRIPT No matching script. Please use EVAL. ``` -In redis-py, this error is `NoScriptError: No matching script. Please use EVAL.` +## In client libraries + +{{< error-clients >}} ## Example diff --git a/content/develop/reference/errors/common/oom.md b/content/develop/reference/errors/common/oom.md index ed5f30ffdb..2c4ef9e357 100644 --- a/content/develop/reference/errors/common/oom.md +++ b/content/develop/reference/errors/common/oom.md @@ -28,10 +28,12 @@ Redis returns this error when it has reached its memory limit and can't free mem OOM command not allowed when used memory > 'maxmemory'. ``` -In redis-py, this error is `OutOfMemoryError: command not allowed when used memory > 'maxmemory'.` - A Lua function, or a script that starts with a `#!lua` line, gets the same error before it starts, unless it has the `allow-oom` or `no-writes` flag. A script without that line gets the error when it calls a command that adds data, with the script's details added to the end. +## In client libraries + +{{< error-clients >}} + ## Example ``` diff --git a/content/develop/reference/errors/common/readonly.md b/content/develop/reference/errors/common/readonly.md index 7bc5c9279e..27c14cbe0f 100644 --- a/content/develop/reference/errors/common/readonly.md +++ b/content/develop/reference/errors/common/readonly.md @@ -28,10 +28,12 @@ Redis returns this error when you send a command that changes data to a replica. READONLY You can't write against a read only replica. ``` -In redis-py, this error is `ReadOnlyError: You can't write against a read only replica.` - A Lua function, or a script that starts with a `#!lua` line, gets the same error on a replica before it starts, unless it has the `no-writes` flag. A script without that line gets the error when it calls a command that changes data, with the script's details added to the end. +## In client libraries + +{{< error-clients >}} + ## Example ``` diff --git a/content/develop/reference/errors/common/tryagain.md b/content/develop/reference/errors/common/tryagain.md index 98010fe858..8198ee15e8 100644 --- a/content/develop/reference/errors/common/tryagain.md +++ b/content/develop/reference/errors/common/tryagain.md @@ -28,10 +28,12 @@ A Redis cluster returns this error when a command with several keys runs while t TRYAGAIN Multiple keys request during rehashing of slot ``` -In redis-py, this error is `TryAgainError: Multiple keys request during rehashing of slot`. - A similar error, `TRYAGAIN Slot is being trimmed`, means that a write reached a node while it was deleting all the keys of the slot, for example, after `SFLUSH`. It's temporary too. +## In client libraries + +{{< error-clients >}} + ## Example During the move, the old node still has `user:{42}:profile`, but `user:{42}:cart` is already on the new node: diff --git a/content/develop/reference/errors/common/wrongpass.md b/content/develop/reference/errors/common/wrongpass.md index eb76358f35..9c4d7596bc 100644 --- a/content/develop/reference/errors/common/wrongpass.md +++ b/content/develop/reference/errors/common/wrongpass.md @@ -19,6 +19,8 @@ error: match: - invalid username-password pair - called without any password configured + - AuthenticationError + - JedisAccessControlException --- Redis returns this error from [`AUTH`](/content/commands/auth.md) or [`HELLO`](/content/commands/hello.md) when it can't authenticate you. For security, the message is the same in each of these cases: @@ -31,7 +33,9 @@ Redis returns this error from [`AUTH`](/content/commands/auth.md) or [`HELLO`](/ WRONGPASS invalid username-password pair or user is disabled. ``` -In redis-py, this error is `AuthenticationError: invalid username-password pair or user is disabled.` +## In client libraries + +{{< error-clients >}} ## How to fix it diff --git a/content/develop/reference/errors/common/wrongtype.md b/content/develop/reference/errors/common/wrongtype.md index c15ebce546..aa3a0df40f 100644 --- a/content/develop/reference/errors/common/wrongtype.md +++ b/content/develop/reference/errors/common/wrongtype.md @@ -30,6 +30,10 @@ WRONGTYPE Operation against a key holding the wrong kind of value Some commands word this error differently. JSON commands return `Existing key has wrong Redis type`. HyperLogLog commands return `WRONGTYPE Key is not a valid HyperLogLog string value.` for a key that holds a string but not a HyperLogLog. +## In client libraries + +{{< error-clients >}} + ## Example [`GET`](/content/commands/get.md) works with strings, so it fails on a key that holds a list: diff --git a/data/client-exceptions.json b/data/client-exceptions.json new file mode 100644 index 0000000000..edc16a094a --- /dev/null +++ b/data/client-exceptions.json @@ -0,0 +1,304 @@ +{ + "about": "The exception that each client library raises for the error pages in content/develop/reference/errors/common/, and what its message looks like. Keyed by page file name. Both clients were run against a live server, with the same scenarios as build/errors/live_checks.py: redis-py as recorded in build/errors/live_baseline.json, and Jedis on redis:8.10 (server 8.10.2) with the version below.", + "versions": { + "redis-py": "7.4.1", + "jedis": "8.0.2" + }, + "order": [ + "redis-py", + "jedis" + ], + "clients": { + "redis-py": { + "name": "redis-py", + "package": "redis.exceptions", + "generic": "ResponseError", + "basis": "live" + }, + "jedis": { + "name": "Jedis", + "package": "redis.clients.jedis.exceptions", + "generic": "JedisDataException", + "basis": "live" + } + }, + "errors": { + "ask": { + "redis-py": { + "class": "AskError", + "message": " :", + "note": "`RedisCluster` follows the redirect itself and raises this error only if it can't." + }, + "jedis": { + "class": "JedisAskDataException", + "message": "ASK :", + "note": "`JedisCluster` follows the redirect itself and raises this error only if it can't." + } + }, + "busy": { + "redis-py": { + "class": "ResponseError", + "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE." + }, + "jedis": { + "class": "JedisBusyException", + "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE." + } + }, + "busygroup": { + "redis-py": { + "class": "ResponseError", + "message": "BUSYGROUP Consumer Group name already exists" + }, + "jedis": { + "class": "JedisDataException", + "message": "BUSYGROUP Consumer Group name already exists" + } + }, + "busykey": { + "redis-py": { + "class": "ResponseError", + "message": "BUSYKEY Target key name already exists." + }, + "jedis": { + "class": "JedisDataException", + "message": "BUSYKEY Target key name already exists." + } + }, + "clusterdown": { + "redis-py": { + "class": "ClusterDownError", + "message": "The cluster is down" + }, + "jedis": { + "class": "JedisClusterException", + "message": "CLUSTERDOWN The cluster is down" + } + }, + "crossslot": { + "redis-py": { + "class": "ClusterCrossSlotError", + "message": "Keys in request don't hash to the same slot" + }, + "jedis": { + "class": "JedisDataException", + "message": "CROSSSLOT Keys in request don't hash to the same slot" + } + }, + "denied": { + "redis-py": { + "class": "ConnectionError", + "message": "Connection closed by server." + }, + "jedis": { + "class": "JedisConnectionException", + "message": "Unexpected end of stream." + } + }, + "err-max-clients": { + "redis-py": { + "class": "ConnectionError", + "message": "max number of clients reached" + }, + "jedis": { + "class": "JedisConnectionException", + "message": "Unexpected end of stream." + } + }, + "err-not-an-integer": { + "redis-py": { + "class": "ResponseError", + "message": "value is not an integer or out of range" + }, + "jedis": { + "class": "JedisDataException", + "message": "ERR value is not an integer or out of range" + } + }, + "err-syntax-error": { + "redis-py": { + "class": "ResponseError", + "message": "syntax error" + }, + "jedis": { + "class": "JedisDataException", + "message": "ERR syntax error" + } + }, + "err-unknown-command": { + "redis-py": { + "class": "ResponseError", + "message": "unknown command '', with args beginning with: " + }, + "jedis": { + "class": "JedisDataException", + "message": "ERR unknown command '', with args beginning with: " + } + }, + "err-wrong-number-of-arguments": { + "redis-py": { + "class": "ResponseError", + "message": "wrong number of arguments for '' command" + }, + "jedis": { + "class": "JedisDataException", + "message": "ERR wrong number of arguments for '' command" + } + }, + "execabort": { + "redis-py": { + "class": "ResponseError", + "message": "Command # () of pipeline caused error: ", + "note": "redis-py raises the error of the command that failed to queue, not `EXECABORT`." + }, + "jedis": { + "class": "JedisDataException", + "message": "EXECABORT Transaction discarded because of previous errors.", + "note": "The errors of the commands that failed to queue are attached to the exception as suppressed exceptions." + } + }, + "loading": { + "redis-py": { + "class": "BusyLoadingError", + "message": "Redis is loading the dataset in memory" + }, + "jedis": { + "class": "JedisDataException", + "message": "LOADING Redis is loading the dataset in memory" + } + }, + "masterdown": { + "redis-py": { + "class": "MasterDownError", + "message": "Link with MASTER is down and replica-serve-stale-data is set to 'no'." + }, + "jedis": { + "class": "JedisDataException", + "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + } + }, + "misconf": { + "redis-py": { + "class": "ResponseError", + "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + }, + "jedis": { + "class": "JedisDataException", + "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + } + }, + "moved": { + "redis-py": { + "class": "MovedError", + "message": " :", + "note": "`RedisCluster` follows the redirect itself and raises this error only if it can't." + }, + "jedis": { + "class": "JedisMovedDataException", + "message": "MOVED :", + "note": "`JedisCluster` follows the redirect itself and raises this error only if it can't." + } + }, + "noauth": { + "redis-py": { + "class": "AuthenticationError", + "message": "Authentication required." + }, + "jedis": { + "class": "JedisAccessControlException", + "message": "NOAUTH Authentication required." + } + }, + "nogroup": { + "redis-py": { + "class": "ResponseError", + "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option" + }, + "jedis": { + "class": "JedisDataException", + "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option" + } + }, + "noperm": { + "redis-py": { + "class": "NoPermissionError", + "message": "User has no permissions to run the '' command" + }, + "jedis": { + "class": "JedisAccessControlException", + "message": "NOPERM User has no permissions to run the '' command" + } + }, + "noreplicas": { + "redis-py": { + "class": "ResponseError", + "message": "NOREPLICAS Not enough good replicas to write." + }, + "jedis": { + "class": "JedisDataException", + "message": "NOREPLICAS Not enough good replicas to write." + } + }, + "noscript": { + "redis-py": { + "class": "NoScriptError", + "message": "No matching script. Please use EVAL." + }, + "jedis": { + "class": "JedisNoScriptException", + "message": "NOSCRIPT No matching script. Please use EVAL." + } + }, + "oom": { + "redis-py": { + "class": "OutOfMemoryError", + "message": "command not allowed when used memory > 'maxmemory'." + }, + "jedis": { + "class": "JedisDataException", + "message": "OOM command not allowed when used memory > 'maxmemory'." + } + }, + "readonly": { + "redis-py": { + "class": "ReadOnlyError", + "message": "You can't write against a read only replica." + }, + "jedis": { + "class": "JedisDataException", + "message": "READONLY You can't write against a read only replica." + } + }, + "tryagain": { + "redis-py": { + "class": "TryAgainError", + "message": "Multiple keys request during rehashing of slot" + }, + "jedis": { + "class": "JedisDataException", + "message": "TRYAGAIN Multiple keys request during rehashing of slot" + } + }, + "wrongpass": { + "redis-py": { + "class": "AuthenticationError", + "message": "invalid username-password pair or user is disabled." + }, + "jedis": { + "class": "JedisAccessControlException", + "message": "WRONGPASS invalid username-password pair or user is disabled.", + "note": "`AUTH` with a password for a user that has none returns `ERR AUTH called without any password configured for the default user\u2026`, which is a `JedisDataException`." + } + }, + "wrongtype": { + "redis-py": { + "class": "ResponseError", + "message": "WRONGTYPE Operation against a key holding the wrong kind of value" + }, + "jedis": { + "class": "JedisDataException", + "message": "WRONGTYPE Operation against a key holding the wrong kind of value" + } + } + } +} diff --git a/layouts/partials/errors/client-rows.html b/layouts/partials/errors/client-rows.html new file mode 100644 index 0000000000..6413ce8e0e --- /dev/null +++ b/layouts/partials/errors/client-rows.html @@ -0,0 +1,26 @@ +{{- /* + The client library exceptions of one error page, shared by the HTML shortcode + and its Markdown expansion. + + Input: the page's file name without the extension, such as "readonly". + + Returns a slice of dicts, one per client, in the order of "order" in data/client-exceptions.json: + client, package, class, message, note. Empty when the page has no entry. +*/ -}} +{{- $data := index site.Data "client-exceptions" -}} +{{- $rows := slice -}} +{{- with index $data.errors . -}} + {{- $entry := . -}} + {{- range $id := $data.order -}} + {{- $client := index $data.clients $id -}} + {{- with index $entry $id -}} + {{- $rows = $rows | append (dict + "client" $client.name + "package" $client.package + "class" .class + "message" .message + "note" (.note | default "")) -}} + {{- end -}} + {{- end -}} +{{- end -}} +{{- return $rows -}} diff --git a/layouts/partials/markdown-error-clients.html b/layouts/partials/markdown-error-clients.html new file mode 100644 index 0000000000..48660fcc32 --- /dev/null +++ b/layouts/partials/markdown-error-clients.html @@ -0,0 +1,35 @@ +{{- /* + Expand error-clients shortcodes for Markdown/JSON output. + + The HTML shortcode (layouts/shortcodes/error-clients.html) renders the + exceptions that client libraries raise for an error page. AI-facing output + needs the same table as Markdown. Both read the rows from + layouts/partials/errors/client-rows.html. + + Input: dict with: + - "RawContent": The Markdown content to process + - "Page": The current Hugo page + + Output: Markdown content with error-clients shortcodes expanded +*/ -}} + +{{- $content := .RawContent -}} +{{- $matches := findRE `\{\{<\s*error-clients\s*>\}\}` $content -}} +{{- /* A generated page, such as a section without an _index.md, has no .File. */ -}} +{{- $rows := slice -}} +{{- if and $matches .Page.File -}} + {{- $rows = partial "errors/client-rows.html" .Page.File.TranslationBaseName -}} +{{- end -}} + +{{- range $match := $matches -}} + {{- $md := "| Client library | Exception | Message |\n|---|---|---|\n" -}} + {{- range $rows -}} + {{- $msg := replace .message "|" "\\|" -}} + {{- $msg = cond (in $msg "`") (printf "`` %s ``" $msg) (printf "`%s`" $msg) -}} + {{- with .note -}}{{- $msg = printf "%s %s" $msg . -}}{{- end -}} + {{- $md = printf "%s| %s | `%s` | %s |\n" $md .client .class $msg -}} + {{- end -}} + {{- $content = replace $content $match $md 1 -}} +{{- end -}} + +{{- return $content -}} diff --git a/layouts/partials/process-markdown-content.html b/layouts/partials/process-markdown-content.html index 937fcef73c..2a361f979d 100644 --- a/layouts/partials/process-markdown-content.html +++ b/layouts/partials/process-markdown-content.html @@ -123,6 +123,7 @@ {{- $content = partial "markdown-table-children.html" (dict "RawContent" $content "Page" .Page) -}} {{- /* Same for the lists of all error messages, whose body is the list. */ -}} {{- $content = partial "markdown-error-catalog.html" (dict "RawContent" $content "Page" .Page) -}} +{{- $content = partial "markdown-error-clients.html" (dict "RawContent" $content "Page" .Page) -}} {{- /* Unescape again, as Hugo re-escapes partial output. Must cover the same entities as the block above, including +, or a "+" in a table cell (a version column such as "6.0+", say) reaches the feed as a literal entity. */ -}} diff --git a/layouts/shortcodes/error-clients.html b/layouts/shortcodes/error-clients.html new file mode 100644 index 0000000000..4da7afd889 --- /dev/null +++ b/layouts/shortcodes/error-clients.html @@ -0,0 +1,30 @@ +{{- /* + error-clients + ------------- + Lists the exception that each client library raises for the error on this + page, from data/client-exceptions.json. The entry is the page's file name, + so the shortcode takes no arguments. + + Usage, on a page in errors/common: + {{< error-clients >}} + + layouts/partials/markdown-error-clients.html renders the same table for the + Markdown output, through the same layouts/partials/errors/client-rows.html. +*/ -}} +{{- $rows := partial "errors/client-rows.html" .Page.File.TranslationBaseName -}} +{{- $v := substr (md5 (os.ReadFile "static/css/errors.css")) 0 10 }} + +
+ + + + {{- range $rows }} + + + + + + {{- end }} + +
Client libraryExceptionMessage
{{ .client }}{{ .class }}{{ .message }}{{ with .note }}
{{ . | markdownify }}{{ end }}
+
From 0502721d628956a51b96a31ccaed6235f5b0253d Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Tue, 3 Nov 2026 14:08:09 +0000 Subject: [PATCH 02/11] DOC-7253 Move the Jedis live check into build/errors as a reusable client bridge live_clients.py runs the checks of live_checks.py with another client library, so each new client in the error reference needs a small bridge and probe, not another copy of the script. A bridge in bridges/ turns the redis-py calls that the checks make into raw commands for a probe program in the client's language, and the probe prints the exception class and message. The Jedis bridge is the first, and client_baselines/jedis.json holds its result. live_checks.py gets two hooks (CLIENT_KEY and EXTRA_META) and nothing else, so live_baseline.json and the verify step behave as before. The existing tests still pass. Running the Jedis bridge through the new path gave exactly the replies of the earlier scratchpad run, which is how I checked that the move changed nothing. Learned: the bridge replaces py_err and client in the live_checks module at run time, so scenario code stays client-neutral and a new client needs no change to it Constraint: live_baseline.json must keep only redis-py names under redis_py, because compare() and verify --accept key off that Directive: Put a new client's probe and bridge in bridges/ and run it through live_clients.py; do not copy live_checks.py Gaps: The Jedis bridge needs Maven Central and a working mvn; ERRORS_CACHE controls where it builds Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- build/errors/README.md | 25 ++ build/errors/bridges/Probe.java | 45 +++ build/errors/bridges/_common.py | 86 ++++++ build/errors/bridges/jedis.py | 33 ++ build/errors/client_baselines/jedis.json | 367 +++++++++++++++++++++++ build/errors/live_checks.py | 8 +- build/errors/live_clients.py | 63 ++++ 7 files changed, 625 insertions(+), 2 deletions(-) create mode 100644 build/errors/bridges/Probe.java create mode 100644 build/errors/bridges/_common.py create mode 100644 build/errors/bridges/jedis.py create mode 100644 build/errors/client_baselines/jedis.json create mode 100644 build/errors/live_clients.py diff --git a/build/errors/README.md b/build/errors/README.md index 0f3006a42a..9166176580 100644 --- a/build/errors/README.md +++ b/build/errors/README.md @@ -111,6 +111,29 @@ checking what Claude did. 7. **Build the site and open a pull request.** Look at the list pages that changed, and at the error lookup on the errors page. +## Check how client libraries report the errors + +The "In client libraries" table on each error page comes from +`data/client-exceptions.json`. `verify` records only the redis-py names. To check +another client library, run the same checks with that client: + +``` +python3 build/errors/live_clients.py jedis +python3 build/errors/live_clients.py node_redis --only standalone +``` + +It needs Docker, like `verify`, and the client's tools: Java and Maven for Jedis, +Node.js and npm for node-redis. Each client is a bridge in `bridges/`: a short +Python module and a probe program in the client's language that runs one command +and prints the exception class and message. The run writes +`client_baselines/.json`. Copy the classes and messages that the pages +show into `data/client-exceptions.json` by hand, and add the client to its +`order` list. The data file says which clients were run live. + +To add a client, copy `bridges/jedis.py` and its probe, and make the probe print +`OKvalue` or `EXCclassmessage`. The format is in +`bridges/_common.py`. + ## How the comparison works The tool reads every string in the C and Rust source of each repository, at @@ -132,6 +155,8 @@ builds, can't be followed. `diff` says how many. - `live_checks.py`: the server checks that `verify` runs. - `live_baseline.json`: the replies that the error pages show, from the last accepted run. +- `live_clients.py`, `bridges/` and `client_baselines/`: the same checks run + with other client libraries. - `explanation-style.md`: how to write an explanation, and the fields of a new message. - `../test_update_errors.py`: unit tests. Run `python3 build/test_update_errors.py`. diff --git a/build/errors/bridges/Probe.java b/build/errors/bridges/Probe.java new file mode 100644 index 0000000000..1363d97597 --- /dev/null +++ b/build/errors/bridges/Probe.java @@ -0,0 +1,45 @@ +import redis.clients.jedis.*; +import redis.clients.jedis.commands.ProtocolCommand; +import java.util.*; + +/** Runs one command, or one MULTI/EXEC block, through Jedis and prints what it throws. */ +public class Probe { + static byte[] arg(String s) { + if (s.startsWith("hex:")) { + String h = s.substring(4); byte[] b = new byte[h.length() / 2]; + for (int i = 0; i < b.length; i++) b[i] = (byte) Integer.parseInt(h.substring(2 * i, 2 * i + 2), 16); + return b; + } + return s.getBytes(); + } + static ProtocolCommand pc(String name) { byte[] raw = name.getBytes(); return () -> raw; } + static byte[][] rest(String[] t, int from) { + byte[][] a = new byte[t.length - from][]; + for (int i = from; i < t.length; i++) a[i - from] = arg(t[i]); + return a; + } + static String one(Throwable e) { return String.valueOf(e.getMessage()).replace("\r", " ").replace("\n", " "); } + + public static void main(String[] a) { + int port = Integer.parseInt(a[0]); + DefaultJedisClientConfig.Builder b = DefaultJedisClientConfig.builder().connectionTimeoutMillis(5000).socketTimeoutMillis(5000); + if (!a[1].isEmpty()) b.user(a[1]); + if (!a[2].isEmpty()) b.password(a[2]); + try (Jedis j = new Jedis(new HostAndPort("127.0.0.1", port), b.build())) { + if (a[3].equals("cmd")) { + String[] t = a[4].split("\u0001", -1); + Object r = j.sendCommand(pc(t[0]), rest(t, 1)); + System.out.println("OK\t" + (r instanceof byte[] ? new String((byte[]) r) : r)); + } else { + Transaction tx = j.multi(); + for (int i = 4; i < a.length; i++) { String[] t = a[i].split("\u0001", -1); tx.sendCommand(pc(t[0]), rest(t, 1)); } + List r = tx.exec(); + System.out.println("OK\t" + r); + } + } catch (Throwable e) { + StringBuilder sup = new StringBuilder(); + for (Throwable t : e.getSuppressed()) sup.append(" [suppressed ").append(t.getClass().getSimpleName()).append(": ").append(one(t)).append("]"); + System.out.println("EXC\t" + e.getClass().getName() + "\t" + one(e) + sup); + } + } +} diff --git a/build/errors/bridges/_common.py b/build/errors/bridges/_common.py new file mode 100644 index 0000000000..b4d2de7f54 --- /dev/null +++ b/build/errors/bridges/_common.py @@ -0,0 +1,86 @@ +"""The part of every client bridge that doesn't depend on the client library. + +A bridge lets live_checks.py run its checks with another client library. The +checks call `client(...)` and `py_err(...)` as if the client were redis-py, so a +bridge offers the redis-py methods that the checks use, and turns each one into +a raw command for a small probe program written in the client's language. The +probe runs the command and prints what the client returns or raises. + +A bridge module defines: + + KEY the key the exceptions are recorded under, such as 'jedis' + VERSION the default client version + prepare(version, cache) installs the client and the probe (once), returns a + function run(port, user, password, mode, *payload) + that returns the probe's output, or raises ClientErr +""" +import os, subprocess + +SEP = '\x01' + + +class ClientErr(Exception): + """An error that the client raised: its class name and message.""" + def __init__(self, cls, msg): + super().__init__(f'{cls}: {msg}') + self.cls, self.msg = cls, msg + + +def parse(stdout, stderr=''): + """The probe prints one line, `OKvalue` or `EXCclassmessage`.""" + lines = [l for l in stdout.splitlines() if l.startswith(('OK\t', 'EXC\t'))] + if not lines: + raise RuntimeError(f'the probe printed no result: {stdout!r} {stderr[-300:]!r}') + parts = lines[-1].split('\t', 2) + if parts[0] == 'EXC': + raise ClientErr(parts[1], parts[2] if len(parts) > 2 else '') + return parts[1] + + +def sh(*cmd, cwd=None, env=None): + r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, env={**os.environ, **(env or {})}, timeout=900) + if r.returncode: + raise SystemExit(f'{" ".join(cmd[:3])} failed:\n{r.stdout[-800:]}\n{r.stderr[-800:]}') + return r + + +def _a(x): + # Binary arguments, such as the output of DUMP, travel as hex. + return 'hex:' + x.hex() if isinstance(x, bytes) else str(x) + + +def pack(args): + return SEP.join(_a(x) for x in args) + + +class Bridge: + """The redis-py methods that live_checks.py calls, as raw commands.""" + run = None # set by live_clients.py: run(port, user, password, mode, *payload) + + def __init__(self, port, username=None, password=None): + self.port, self.user, self.pw = port, username or '', password or '' + + def execute_command(self, *args): + return self.run(self.port, self.user, self.pw, 'cmd', pack(args)) + + def get(self, k): return self.execute_command('GET', k) + def set(self, k, v): return self.execute_command('SET', k, v) + def ping(self): return self.execute_command('PING') + def incr(self, k): return self.execute_command('INCR', k) + def evalsha(self, sha, n): return self.execute_command('EVALSHA', sha, n) + def restore(self, name, ttl, data): return self.execute_command('RESTORE', name, ttl, data) + def xgroup_create(self, s, g, i, mkstream=False): + return self.execute_command('XGROUP', 'CREATE', s, g, i, *(['MKSTREAM'] if mkstream else [])) + def xreadgroup(self, g, c, streams): + return self.execute_command('XREADGROUP', 'GROUP', g, c, 'STREAMS', *streams.keys(), *streams.values()) + def mget(self, *keys): return self.execute_command('MGET', *keys) + def mset(self, d): return self.execute_command('MSET', *[x for kv in d.items() for x in kv]) + def pipeline(self, transaction=True): return Tx(self) + + +class Tx: + """A MULTI/EXEC block: the commands queue here, and execute() sends them.""" + def __init__(self, b): self.b, self.cmds = b, [] + def execute_command(self, *args): self.cmds.append(pack(args)) + def set(self, k, v): self.execute_command('SET', k, v) + def execute(self): return self.b.run(self.b.port, self.b.user, self.b.pw, 'tx', *self.cmds) diff --git a/build/errors/bridges/jedis.py b/build/errors/bridges/jedis.py new file mode 100644 index 0000000000..33426119e5 --- /dev/null +++ b/build/errors/bridges/jedis.py @@ -0,0 +1,33 @@ +"""Jedis bridge for live_clients.py. Needs Java 17+ and Maven, and network access to Maven Central.""" +import os, subprocess +from pathlib import Path +from _common import parse, sh + +KEY = 'jedis' +VERSION = '8.0.2' +HERE = Path(__file__).parent + +POM = '''4.0.0 +xprobe1 +redis.clientsjedis{v} +''' + + +def prepare(version, cache): + d = Path(cache) / 'bridges' / f'jedis-{version}' + d.mkdir(parents=True, exist_ok=True) + env = {'JAVA_HOME': ''} # a stale JAVA_HOME breaks mvn on this setup; use the java on the PATH + cp = d / 'cp.txt' + if not cp.exists(): + (d / 'pom.xml').write_text(POM.format(v=version)) + sh('mvn', '-q', '-B', 'dependency:build-classpath', f'-Dmdep.outputFile={cp}', cwd=d, env=env) + for f in ('Probe.java',): + (d / f).write_text((HERE / f).read_text()) + sh('javac', '-cp', cp.read_text().strip(), 'Probe.java', cwd=d, env=env) + classpath = f'{cp.read_text().strip()}:{d}' + + def run(port, user, password, mode, *payload): + out = subprocess.run(['java', '-cp', classpath, 'Probe', str(port), user, password, mode, *payload], + capture_output=True, text=True, timeout=60, env={**os.environ, 'JAVA_HOME': ''}) + return parse(out.stdout, out.stderr) + return run diff --git a/build/errors/client_baselines/jedis.json b/build/errors/client_baselines/jedis.json new file mode 100644 index 0000000000..b41384330e --- /dev/null +++ b/build/errors/client_baselines/jedis.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "jedis": "8.0.2" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: ERR unknown command 'FOO', with args beginning with: 'bar' " + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: ERR wrong number of arguments for 'get' command" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: ERR syntax error" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: ERR value is not an integer or out of range" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: WRONGTYPE Operation against a key holding the wrong kind of value" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: EXECABORT Transaction discarded because of previous errors. [suppressed JedisDataException: ERR unknown command 'FOO']" + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisNoScriptException: NOSCRIPT No matching script. Please use EVAL." + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: BUSYKEY Target key name already exists." + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: BUSYGROUP Consumer Group name already exists" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "redis.clients.jedis.exceptions.JedisAccessControlException: WRONGPASS invalid username-password pair or user is disabled." + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'." + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisBusyException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE." + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream." + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: NOREPLICAS Not enough good replicas to write." + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-107.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisAccessControlException: NOAUTH Authentication required." + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisAccessControlException: NOPERM User app has no permissions to run the 'set' command", + "redis.clients.jedis.exceptions.JedisAccessControlException: NOPERM No permissions to access a key" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream." + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: READONLY You can't write against a read only replica." + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793714857", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2695760", + "loading_loaded_bytes:7166", + "loading_loaded_perc:1.07", + "loading_eta_seconds:1" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisMovedDataException: MOVED 12182 127.0.0.1:6573" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: CROSSSLOT Keys in request don't hash to the same slot" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisAskDataException: ASK 12182 127.0.0.1:6571" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisDataException: TRYAGAIN Multiple keys request during rehashing of slot" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "jedis": [ + "redis.clients.jedis.exceptions.JedisClusterException: CLUSTERDOWN Hash slot not served", + "redis.clients.jedis.exceptions.JedisClusterException: CLUSTERDOWN The cluster is down" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/build/errors/live_checks.py b/build/errors/live_checks.py index 2d006e3dd0..b4e370d4c4 100644 --- a/build/errors/live_checks.py +++ b/build/errors/live_checks.py @@ -20,6 +20,10 @@ CRUN = '/tmp/rerr' PORTS = '127.0.0.1:6550-6575:6550-6575' results = {} +# What rec() files a client's exception under. live_clients.py sets these to run the same +# checks with another client library, and leaves live_baseline.json alone. +CLIENT_KEY = 'redis_py' +EXTRA_META = {} class _Missing(Exception): @@ -108,7 +112,7 @@ def cmd(self, *args): def rec(key, raw=None, py=None, note=None): r = results.setdefault(key, {}) if raw is not None: r.setdefault('raw', []).append(raw) - if py is not None: r.setdefault('redis_py', []).append(py) + if py is not None: r.setdefault(CLIENT_KEY, []).append(py) if note: r.setdefault('notes', []).append(note) print(key, raw, py, flush=True) @@ -430,7 +434,7 @@ def run(image, out, only=()): dx('mkdir', '-p', CRUN, user='root'); dx('chown', 'redis', CRUN, user='root') ver = dx('redis-server', '--version').stdout.strip() results['_meta'] = {'redis': ver, 'redis_py': redis.__version__ if redis else None, - 'date': time.strftime('%Y-%m-%d')} + 'date': time.strftime('%Y-%m-%d'), **EXTRA_META} for step in STEPS: if only and step.__name__ not in only: continue try: diff --git a/build/errors/live_clients.py b/build/errors/live_clients.py new file mode 100644 index 0000000000..396a8843b4 --- /dev/null +++ b/build/errors/live_clients.py @@ -0,0 +1,63 @@ +"""Run the checks of live_checks.py with another client library. + + python3 build/errors/live_clients.py jedis + python3 build/errors/live_clients.py node_redis --version 6.3.1 --only standalone + +It reproduces the same errors on the same throwaway Docker container as +live_checks.py, but sends each command through the client library of a bridge in +bridges/. It records the exception class and message that the client raises, +under the bridge's KEY, in client_baselines/.json. It doesn't touch +live_baseline.json. data/client-exceptions.json is written by hand from the +result. +""" +import argparse, importlib.util, json, os, sys +from pathlib import Path + +HERE = Path(__file__).parent +sys.path.insert(0, str(HERE)); sys.path.insert(0, str(HERE / 'bridges')) +import live_checks as lc +from _common import Bridge, ClientErr + +CACHE = Path(os.environ.get('ERRORS_CACHE', Path.home() / '.cache' / 'redis-docs-errors')) + + +def load(name): + spec = importlib.util.spec_from_file_location(name, HERE / 'bridges' / f'{name}.py') + mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod) + return mod + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.split('\n')[0]) + ap.add_argument('client', help='a bridge in build/errors/bridges/, such as jedis or node_redis') + ap.add_argument('--version', help='the client version (default: the bridge\'s)') + ap.add_argument('--image', default='redis:8.10', help='the Redis Docker image (default: redis:8.10)') + ap.add_argument('--out', help='where to write the result (default: client_baselines/.json)') + ap.add_argument('--only', default='', help='comma-separated steps to run, such as standalone,cluster') + a = ap.parse_args() + + bridge = load(a.client) + version = a.version or bridge.VERSION + Bridge.run = staticmethod(bridge.prepare(version, CACHE)) + + def py_err(fn): + try: + fn(); return 'no error' + except ClientErr as e: + return f'{e.cls}: {e.msg}' + + lc.redis = None # no redis-py: the result has only this client's names + lc.client = lambda **kw: Bridge(**kw) + lc.py_err = py_err + lc.CLIENT_KEY = bridge.KEY + lc.EXTRA_META = {bridge.KEY: version} + out = Path(a.out) if a.out else HERE / 'client_baselines' / f'{a.client}.json' + out.parent.mkdir(exist_ok=True) + only = tuple(s for s in a.only.split(',') if s) + result = lc.run(a.image, str(out), only=only) + bad = result.get('_failures') + print(f'\nWrote {out}' + (f'\nFAILED STEPS: {bad}' if bad else '')) + + +if __name__ == '__main__': + main() From 4d87a28cbf0d163e8d66704e85196ab9b7b38dd0 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Tue, 3 Nov 2026 14:10:49 +0000 Subject: [PATCH 03/11] DOC-7253 Add node-redis to the client exceptions on the common error pages Runs node-redis 6.3.1 through the live checks (bridges/node_redis.py and its probe, result in client_baselines/node_redis.json) and adds it to data/client-exceptions.json after redis-py and Jedis. node-redis maps no prefix to a class. Every server error is a SimpleError that keeps the full message, and callers have to test err.message.startsWith. So its column is the same class on all 27 pages. That is accurate, and it is the point of the table: a reader who sees ReadOnlyError in redis-py and JedisAccessControlException in Jedis learns that node-redis has nothing to catch by class. Two results differ from the server text. node-redis 6.3.1 sends HELLO when it connects, so NOAUTH surfaces from connect() with "NOAUTH HELLO must be called with the client already authenticated..." and not "NOAUTH Authentication required." Reading the page's example against that message would confuse a reader, so the row carries the real message and a note. And exec() rejects with the error of the command that failed to queue, not EXECABORT, like redis-py. Unlike Jedis, node-redis receives the DENIED and max-clients errors as replies, so they are SimpleError with the server text and not a connection error. SimpleError, like the other generic classes, stays out of each page's match list. Learned: the same server error can reach the reader as a prefix-specific class (redis-py, Jedis) or as one generic class with the prefix in the message (node-redis), and a client's connect handshake can change which error and message you see Constraint: node-redis rows must stay tied to the version in data/client-exceptions.json, because the HELLO-on-connect behavior is version-dependent Gaps: The createCluster redirect note (follows ASK and MOVED, throws MaxCommandRedirectionsError after 16 attempts) comes from cluster/index.js, not a live cluster client run Recheck: when node-redis changes its default protocol or its handshake, or adds per-prefix error classes Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- build/errors/bridges/node_redis.py | 23 ++ build/errors/bridges/node_redis_probe.mjs | 38 ++ build/errors/client_baselines/node_redis.json | 367 ++++++++++++++++++ data/client-exceptions.json | 126 +++++- 4 files changed, 551 insertions(+), 3 deletions(-) create mode 100644 build/errors/bridges/node_redis.py create mode 100644 build/errors/bridges/node_redis_probe.mjs create mode 100644 build/errors/client_baselines/node_redis.json diff --git a/build/errors/bridges/node_redis.py b/build/errors/bridges/node_redis.py new file mode 100644 index 0000000000..a8a541765e --- /dev/null +++ b/build/errors/bridges/node_redis.py @@ -0,0 +1,23 @@ +"""node-redis bridge for live_clients.py. Needs Node.js 18+ and npm, and network access to the npm registry.""" +import os, subprocess +from pathlib import Path +from _common import parse, sh + +KEY = 'node_redis' +VERSION = '6.3.1' +HERE = Path(__file__).parent + + +def prepare(version, cache): + d = Path(cache) / 'bridges' / f'node-redis-{version}' + d.mkdir(parents=True, exist_ok=True) + if not (d / 'node_modules' / 'redis').exists(): + (d / 'package.json').write_text('{"type":"module"}') + sh('npm', 'install', '--no-audit', '--no-fund', '--cache', str(d / '.npm-cache'), f'redis@{version}', cwd=d) + (d / 'probe.mjs').write_text((HERE / 'node_redis_probe.mjs').read_text()) + + def run(port, user, password, mode, *payload): + out = subprocess.run(['node', 'probe.mjs', str(port), user, password, mode, *payload], cwd=d, + capture_output=True, text=True, timeout=60, env=os.environ) + return parse(out.stdout, out.stderr) + return run diff --git a/build/errors/bridges/node_redis_probe.mjs b/build/errors/bridges/node_redis_probe.mjs new file mode 100644 index 0000000000..e836fa5f94 --- /dev/null +++ b/build/errors/bridges/node_redis_probe.mjs @@ -0,0 +1,38 @@ +// Runs one command, or one MULTI/EXEC block, through node-redis and prints what it returns or throws. +// Arguments: port user password mode payload... (see _common.py for the format) +import { createClient } from 'redis'; + +const [port, user, password, mode, ...payload] = process.argv.slice(2); +const arg = (s) => (s.startsWith('hex:') ? Buffer.from(s.slice(4), 'hex') : s); +const tokens = (p) => p.split('\x01').map(arg); +const one = (s) => String(s).replace(/[\r\n]+/g, ' '); + +function describe(e) { + let msg = one(e.message); + // A transaction whose commands failed reports each error in the list it throws. + if (e.replies) msg += ' [errors: ' + e.errorIndexes.map((i) => `${e.replies[i]?.constructor?.name}: ${one(e.replies[i]?.message)}`).join('; ') + ']'; + return `EXC\t${e.constructor.name}\t${msg}`; +} + +const client = createClient({ + socket: { host: '127.0.0.1', port: Number(port), connectTimeout: 5000, reconnectStrategy: false }, + ...(user ? { username: user } : {}), + ...(password ? { password } : {}), +}); +client.on('error', () => {}); + +try { + await client.connect(); + if (mode === 'cmd') { + const r = await client.sendCommand(tokens(payload[0])); + console.log('OK\t' + one(r)); + } else { + const m = client.multi(); + for (const p of payload) m.addCommand(tokens(p)); + console.log('OK\t' + one(await m.exec())); + } +} catch (e) { + console.log(describe(e)); +} finally { + client.destroy(); +} diff --git a/build/errors/client_baselines/node_redis.json b/build/errors/client_baselines/node_redis.json new file mode 100644 index 0000000000..559c4aa509 --- /dev/null +++ b/build/errors/client_baselines/node_redis.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "node_redis": "6.3.1" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "node_redis": [ + "SimpleError: ERR unknown command 'FOO', with args beginning with: 'bar' " + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "node_redis": [ + "SimpleError: ERR wrong number of arguments for 'get' command" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "node_redis": [ + "SimpleError: ERR syntax error" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "node_redis": [ + "SimpleError: ERR value is not an integer or out of range" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "node_redis": [ + "SimpleError: WRONGTYPE Operation against a key holding the wrong kind of value" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "node_redis": [ + "SimpleError: ERR unknown command 'FOO'" + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "node_redis": [ + "SimpleError: NOSCRIPT No matching script. Please use EVAL." + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "node_redis": [ + "SimpleError: BUSYKEY Target key name already exists." + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "node_redis": [ + "SimpleError: BUSYGROUP Consumer Group name already exists" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "node_redis": [ + "SimpleError: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "node_redis": [ + "SimpleError: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "SimpleError: WRONGPASS invalid username-password pair or user is disabled." + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "node_redis": [ + "SimpleError: OOM command not allowed when used memory > 'maxmemory'." + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "node_redis": [ + "SimpleError: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE." + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "node_redis": [ + "SimpleError: ERR max number of clients reached" + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "node_redis": [ + "SimpleError: NOREPLICAS Not enough good replicas to write." + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "node_redis": [ + "SimpleError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-107.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "node_redis": [ + "SimpleError: NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "node_redis": [ + "SimpleError: NOPERM User app has no permissions to run the 'set' command", + "SimpleError: NOPERM No permissions to access a key" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "node_redis": [ + "SimpleError: DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "node_redis": [ + "SimpleError: READONLY You can't write against a read only replica." + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "node_redis": [ + "SimpleError: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "node_redis": [ + "SimpleError: LOADING Redis is loading the dataset in memory" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793714911", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2696704", + "loading_loaded_bytes:10238", + "loading_loaded_perc:1.53", + "loading_eta_seconds:1" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "node_redis": [ + "SimpleError: MOVED 12182 127.0.0.1:6573" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "node_redis": [ + "SimpleError: CROSSSLOT Keys in request don't hash to the same slot" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "node_redis": [ + "SimpleError: ASK 12182 127.0.0.1:6571" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "node_redis": [ + "SimpleError: TRYAGAIN Multiple keys request during rehashing of slot" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "node_redis": [ + "SimpleError: CLUSTERDOWN Hash slot not served", + "SimpleError: CLUSTERDOWN The cluster is down" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/data/client-exceptions.json b/data/client-exceptions.json index edc16a094a..676e65f1b9 100644 --- a/data/client-exceptions.json +++ b/data/client-exceptions.json @@ -1,12 +1,14 @@ { - "about": "The exception that each client library raises for the error pages in content/develop/reference/errors/common/, and what its message looks like. Keyed by page file name. Both clients were run against a live server, with the same scenarios as build/errors/live_checks.py: redis-py as recorded in build/errors/live_baseline.json, and Jedis on redis:8.10 (server 8.10.2) with the version below.", + "about": "The exception that each client library raises for the error pages in content/develop/reference/errors/common/, and what its message looks like. Keyed by page file name. Each client was run against a live server, with the same scenarios as build/errors/live_checks.py: redis-py as recorded in build/errors/live_baseline.json, and the others with build/errors/live_clients.py on redis:8.10 (server 8.10.2), in build/errors/client_baselines/. The notes on MOVED and ASK about cluster clients that follow the redirect come from source.", "versions": { "redis-py": "7.4.1", - "jedis": "8.0.2" + "jedis": "8.0.2", + "node-redis": "6.3.1" }, "order": [ "redis-py", - "jedis" + "jedis", + "node-redis" ], "clients": { "redis-py": { @@ -20,6 +22,12 @@ "package": "redis.clients.jedis.exceptions", "generic": "JedisDataException", "basis": "live" + }, + "node-redis": { + "name": "node-redis", + "package": "redis", + "generic": "SimpleError", + "basis": "live" } }, "errors": { @@ -33,6 +41,11 @@ "class": "JedisAskDataException", "message": "ASK :", "note": "`JedisCluster` follows the redirect itself and raises this error only if it can't." + }, + "node-redis": { + "class": "SimpleError", + "message": "ASK :", + "note": "`createCluster` follows the redirect itself, and throws `MaxCommandRedirectionsError` after 16 attempts by default." } }, "busy": { @@ -43,6 +56,10 @@ "jedis": { "class": "JedisBusyException", "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE." + }, + "node-redis": { + "class": "SimpleError", + "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE." } }, "busygroup": { @@ -53,6 +70,10 @@ "jedis": { "class": "JedisDataException", "message": "BUSYGROUP Consumer Group name already exists" + }, + "node-redis": { + "class": "SimpleError", + "message": "BUSYGROUP Consumer Group name already exists" } }, "busykey": { @@ -63,6 +84,10 @@ "jedis": { "class": "JedisDataException", "message": "BUSYKEY Target key name already exists." + }, + "node-redis": { + "class": "SimpleError", + "message": "BUSYKEY Target key name already exists." } }, "clusterdown": { @@ -73,6 +98,10 @@ "jedis": { "class": "JedisClusterException", "message": "CLUSTERDOWN The cluster is down" + }, + "node-redis": { + "class": "SimpleError", + "message": "CLUSTERDOWN The cluster is down" } }, "crossslot": { @@ -83,6 +112,10 @@ "jedis": { "class": "JedisDataException", "message": "CROSSSLOT Keys in request don't hash to the same slot" + }, + "node-redis": { + "class": "SimpleError", + "message": "CROSSSLOT Keys in request don't hash to the same slot" } }, "denied": { @@ -93,6 +126,10 @@ "jedis": { "class": "JedisConnectionException", "message": "Unexpected end of stream." + }, + "node-redis": { + "class": "SimpleError", + "message": "DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. ..." } }, "err-max-clients": { @@ -103,6 +140,10 @@ "jedis": { "class": "JedisConnectionException", "message": "Unexpected end of stream." + }, + "node-redis": { + "class": "SimpleError", + "message": "ERR max number of clients reached" } }, "err-not-an-integer": { @@ -113,6 +154,10 @@ "jedis": { "class": "JedisDataException", "message": "ERR value is not an integer or out of range" + }, + "node-redis": { + "class": "SimpleError", + "message": "ERR value is not an integer or out of range" } }, "err-syntax-error": { @@ -123,6 +168,10 @@ "jedis": { "class": "JedisDataException", "message": "ERR syntax error" + }, + "node-redis": { + "class": "SimpleError", + "message": "ERR syntax error" } }, "err-unknown-command": { @@ -133,6 +182,10 @@ "jedis": { "class": "JedisDataException", "message": "ERR unknown command '', with args beginning with: " + }, + "node-redis": { + "class": "SimpleError", + "message": "ERR unknown command '', with args beginning with: " } }, "err-wrong-number-of-arguments": { @@ -143,6 +196,10 @@ "jedis": { "class": "JedisDataException", "message": "ERR wrong number of arguments for '' command" + }, + "node-redis": { + "class": "SimpleError", + "message": "ERR wrong number of arguments for '' command" } }, "execabort": { @@ -155,6 +212,11 @@ "class": "JedisDataException", "message": "EXECABORT Transaction discarded because of previous errors.", "note": "The errors of the commands that failed to queue are attached to the exception as suppressed exceptions." + }, + "node-redis": { + "class": "SimpleError", + "message": "ERR unknown command ''", + "note": "`exec()` rejects with the error of the command that failed to queue, not `EXECABORT`." } }, "loading": { @@ -165,6 +227,10 @@ "jedis": { "class": "JedisDataException", "message": "LOADING Redis is loading the dataset in memory" + }, + "node-redis": { + "class": "SimpleError", + "message": "LOADING Redis is loading the dataset in memory" } }, "masterdown": { @@ -175,6 +241,10 @@ "jedis": { "class": "JedisDataException", "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + }, + "node-redis": { + "class": "SimpleError", + "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." } }, "misconf": { @@ -185,6 +255,10 @@ "jedis": { "class": "JedisDataException", "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + }, + "node-redis": { + "class": "SimpleError", + "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." } }, "moved": { @@ -197,6 +271,11 @@ "class": "JedisMovedDataException", "message": "MOVED :", "note": "`JedisCluster` follows the redirect itself and raises this error only if it can't." + }, + "node-redis": { + "class": "SimpleError", + "message": "MOVED :", + "note": "`createCluster` follows the redirect itself, and throws `MaxCommandRedirectionsError` after 16 attempts by default." } }, "noauth": { @@ -207,6 +286,11 @@ "jedis": { "class": "JedisAccessControlException", "message": "NOAUTH Authentication required." + }, + "node-redis": { + "class": "SimpleError", + "message": "NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time", + "note": "node-redis sends `HELLO` when it connects, so `connect()` raises this error, and the message names `HELLO`." } }, "nogroup": { @@ -217,6 +301,10 @@ "jedis": { "class": "JedisDataException", "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option" + }, + "node-redis": { + "class": "SimpleError", + "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option" } }, "noperm": { @@ -227,6 +315,10 @@ "jedis": { "class": "JedisAccessControlException", "message": "NOPERM User has no permissions to run the '' command" + }, + "node-redis": { + "class": "SimpleError", + "message": "NOPERM User has no permissions to run the '' command" } }, "noreplicas": { @@ -237,6 +329,10 @@ "jedis": { "class": "JedisDataException", "message": "NOREPLICAS Not enough good replicas to write." + }, + "node-redis": { + "class": "SimpleError", + "message": "NOREPLICAS Not enough good replicas to write." } }, "noscript": { @@ -247,6 +343,10 @@ "jedis": { "class": "JedisNoScriptException", "message": "NOSCRIPT No matching script. Please use EVAL." + }, + "node-redis": { + "class": "SimpleError", + "message": "NOSCRIPT No matching script. Please use EVAL." } }, "oom": { @@ -257,6 +357,10 @@ "jedis": { "class": "JedisDataException", "message": "OOM command not allowed when used memory > 'maxmemory'." + }, + "node-redis": { + "class": "SimpleError", + "message": "OOM command not allowed when used memory > 'maxmemory'." } }, "readonly": { @@ -267,6 +371,10 @@ "jedis": { "class": "JedisDataException", "message": "READONLY You can't write against a read only replica." + }, + "node-redis": { + "class": "SimpleError", + "message": "READONLY You can't write against a read only replica." } }, "tryagain": { @@ -277,6 +385,10 @@ "jedis": { "class": "JedisDataException", "message": "TRYAGAIN Multiple keys request during rehashing of slot" + }, + "node-redis": { + "class": "SimpleError", + "message": "TRYAGAIN Multiple keys request during rehashing of slot" } }, "wrongpass": { @@ -288,6 +400,10 @@ "class": "JedisAccessControlException", "message": "WRONGPASS invalid username-password pair or user is disabled.", "note": "`AUTH` with a password for a user that has none returns `ERR AUTH called without any password configured for the default user\u2026`, which is a `JedisDataException`." + }, + "node-redis": { + "class": "SimpleError", + "message": "WRONGPASS invalid username-password pair or user is disabled." } }, "wrongtype": { @@ -298,6 +414,10 @@ "jedis": { "class": "JedisDataException", "message": "WRONGTYPE Operation against a key holding the wrong kind of value" + }, + "node-redis": { + "class": "SimpleError", + "message": "WRONGTYPE Operation against a key holding the wrong kind of value" } } } From 82574769ac06c831a94671f6c8a49deddfc0b33b Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Tue, 3 Nov 2026 14:23:57 +0000 Subject: [PATCH 04/11] DOC-7253 Add Lettuce and go-redis to the client exceptions on the common error pages Runs Lettuce 7.9.0.RELEASE (sync, async and reactive) and go-redis 9.22.0 through the live checks, with bridges and probes in build/errors/bridges and results in client_baselines. The table on each page now covers five clients. Its column is "Exception or error type", because go-redis has errors, not exceptions. Lettuce is one entry. The three APIs raised the same exception class and message on all 27 pages once I ignored one artifact: the sync API adds a cause that is a copy of the exception it throws. The only difference is that the async API wraps the exception in an ExecutionException from get(), or a CompletionException from join(), so that goes in a note on the Lettuce client, not in three rows. Lettuce maps BUSY* to RedisBusyException by prefix, so BUSYKEY and BUSYGROUP are RedisBusyException too, though they are different errors. Lettuce also sends HELLO and authenticates while connecting, so NOAUTH, WRONGPASS, DENIED and max-clients come out of connect() as a RedisConnectionException ("Unable to connect to /:") whose cause is the server's error. go-redis has typed errors, but they live in an internal package and callers cannot name them. They reach them with redis.IsMovedError(err) and the other helpers, or redis.HasErrorPrefix. So an entry may carry an optional check, shown as "Test with", and the type column shows the public redis.Error interface. node-redis, which also cannot tell errors apart by class, got the same: err.message.startsWith('PREFIX'), the idiom its own cluster code uses. Where a client's class cannot identify the error, its name stays out of the lookup match list. Only RedisLoadingException, RedisNoScriptException and RedisReadOnlyException are added. RedisBusyException is left out because it covers three pages. Learned: a client can be one library with three APIs and still be one table row, so check whether the exceptions differ before splitting it; the sync/async/reactive difference was wrapping, not class Constraint: the DENIED and max-clients rows depend on whether a client reads the error before the server closes the socket, so they can change with timing, a proxy or the client version Directive: Give a client a check only when its class cannot identify the error, and take the check from what the probe reported as true, not from the client's docs Gaps: The RedisClusterClient (5 redirects) and go-redis ClusterClient (MaxRedirects, 3) notes come from source, not a live cluster client run Gaps: go-redis HasErrorPrefix checks are from the source of error.go, not run per page Recheck: when Lettuce or go-redis changes its error types, its HELLO handshake, or its redirect defaults Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- build/errors/bridges/LettuceProbe.java | 95 +++++ build/errors/bridges/go_redis.py | 23 ++ build/errors/bridges/go_redis_probe.go | 89 +++++ build/errors/bridges/lettuce.py | 34 ++ build/errors/bridges/lettuce_async.py | 8 + build/errors/bridges/lettuce_reactive.py | 8 + build/errors/bridges/lettuce_sync.py | 8 + build/errors/client_baselines/go_redis.json | 367 ++++++++++++++++++ .../client_baselines/lettuce_async.json | 367 ++++++++++++++++++ .../client_baselines/lettuce_reactive.json | 367 ++++++++++++++++++ .../errors/client_baselines/lettuce_sync.json | 367 ++++++++++++++++++ .../reference/errors/common/loading.md | 1 + .../reference/errors/common/noscript.md | 1 + .../reference/errors/common/readonly.md | 1 + data/client-exceptions.json | 300 +++++++++++++- layouts/partials/errors/client-rows.html | 4 +- layouts/partials/markdown-error-clients.html | 8 +- layouts/shortcodes/error-clients.html | 6 +- 18 files changed, 2041 insertions(+), 13 deletions(-) create mode 100644 build/errors/bridges/LettuceProbe.java create mode 100644 build/errors/bridges/go_redis.py create mode 100644 build/errors/bridges/go_redis_probe.go create mode 100644 build/errors/bridges/lettuce.py create mode 100644 build/errors/bridges/lettuce_async.py create mode 100644 build/errors/bridges/lettuce_reactive.py create mode 100644 build/errors/bridges/lettuce_sync.py create mode 100644 build/errors/client_baselines/go_redis.json create mode 100644 build/errors/client_baselines/lettuce_async.json create mode 100644 build/errors/client_baselines/lettuce_reactive.json create mode 100644 build/errors/client_baselines/lettuce_sync.json diff --git a/build/errors/bridges/LettuceProbe.java b/build/errors/bridges/LettuceProbe.java new file mode 100644 index 0000000000..061463359d --- /dev/null +++ b/build/errors/bridges/LettuceProbe.java @@ -0,0 +1,95 @@ +import io.lettuce.core.*; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.reactive.RedisReactiveCommands; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.output.NestedMultiOutput; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.ProtocolKeyword; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.*; + +/** Runs one command, or one MULTI/EXEC block, through Lettuce's sync, async or reactive API and prints what it throws. */ +public class LettuceProbe { + static final StringCodec C = StringCodec.UTF8; + + static ProtocolKeyword kw(String name) { + byte[] raw = name.getBytes(); + return new ProtocolKeyword() { + public byte[] getBytes() { return raw; } + public String name() { return name; } + }; + } + static CommandArgs args(String[] t) { + CommandArgs a = new CommandArgs<>(C); + for (int i = 1; i < t.length; i++) { + if (t[i].startsWith("hex:")) { + String h = t[i].substring(4); byte[] b = new byte[h.length() / 2]; + for (int j = 0; j < b.length; j++) b[j] = (byte) Integer.parseInt(h.substring(2 * j, 2 * j + 2), 16); + a.add(b); + } else a.add(t[i]); + } + return a; + } + static NestedMultiOutput out() { return new NestedMultiOutput<>(C); } + static String one(Throwable e) { return String.valueOf(e.getMessage()).replace("\r", " ").replace("\n", " "); } + + /** The exception the caller meant: the cause of an ExecutionException or CompletionException. */ + static String describe(Throwable e) { + String wrapper = ""; + if ((e instanceof ExecutionException || e instanceof CompletionException) && e.getCause() != null) { + wrapper = " [thrown as " + e.getClass().getSimpleName() + "]"; + e = e.getCause(); + } + String cause = e.getCause() != null ? " [cause " + e.getCause().getClass().getName() + ": " + one(e.getCause()) + "]" : ""; + return "EXC\t" + e.getClass().getName() + "\t" + one(e) + cause + wrapper; + } + + public static void main(String[] a) { + String api = a[0]; int port = Integer.parseInt(a[1]); + RedisURI.Builder ub = RedisURI.builder().withHost("127.0.0.1").withPort(port).withTimeout(Duration.ofSeconds(5)); + if (!a[2].isEmpty() && !a[3].isEmpty()) ub.withAuthentication(a[2], a[3]); + else if (!a[3].isEmpty()) ub.withPassword(a[3].toCharArray()); + RedisClient client = RedisClient.create(ub.build()); + try (StatefulRedisConnection conn = client.connect()) { + Object r; + if (a[4].equals("cmd")) { + String[] t = a[5].split("\u0001", -1); + switch (api) { + case "sync": r = conn.sync().dispatch(kw(t[0]), out(), args(t)); break; + case "async": r = conn.async().dispatch(kw(t[0]), out(), args(t)).get(); break; + default: r = conn.reactive().dispatch(kw(t[0]), out(), args(t)).collectList().block(); + } + } else { + List cmds = new ArrayList<>(); + for (int i = 5; i < a.length; i++) cmds.add(a[i].split("\u0001", -1)); + switch (api) { + case "sync": { + RedisCommands c = conn.sync(); c.multi(); + for (String[] t : cmds) c.dispatch(kw(t[0]), out(), args(t)); + r = c.exec(); break; + } + case "async": { + RedisAsyncCommands c = conn.async(); c.multi().get(); + for (String[] t : cmds) c.dispatch(kw(t[0]), out(), args(t)); + r = c.exec().get(); break; + } + default: { + RedisReactiveCommands c = conn.reactive(); + r = c.multi().flatMap(s -> { + for (String[] t : cmds) c.dispatch(kw(t[0]), out(), args(t)).subscribe(v -> {}, e -> {}); + return c.exec(); + }).block(); + } + } + } + System.out.println("OK\t" + r); + } catch (Throwable e) { + System.out.println(describe(e)); + } finally { + client.shutdown(Duration.ZERO, Duration.ofSeconds(1)); + } + } +} diff --git a/build/errors/bridges/go_redis.py b/build/errors/bridges/go_redis.py new file mode 100644 index 0000000000..afe1aa8231 --- /dev/null +++ b/build/errors/bridges/go_redis.py @@ -0,0 +1,23 @@ +"""go-redis bridge for live_clients.py. Needs Go 1.24+, and network access to the Go module proxy.""" +import os, subprocess +from pathlib import Path +from _common import parse, sh + +KEY = 'go_redis' +VERSION = '9.22.0' +HERE = Path(__file__).parent + + +def prepare(version, cache): + d = Path(cache) / 'bridges' / f'go-redis-{version}' + d.mkdir(parents=True, exist_ok=True) + (d / 'go.mod').write_text(f'module probe\n\ngo 1.24\n\nrequire github.com/redis/go-redis/v9 v{version}\n') + (d / 'main.go').write_text((HERE / 'go_redis_probe.go').read_text()) + sh('go', 'mod', 'tidy', cwd=d) + sh('go', 'build', '-o', 'probe', '.', cwd=d) + + def run(port, user, password, mode, *payload): + out = subprocess.run([str(d / 'probe'), str(port), user, password, mode, *payload], + capture_output=True, text=True, timeout=60, env=os.environ) + return parse(out.stdout, out.stderr) + return run diff --git a/build/errors/bridges/go_redis_probe.go b/build/errors/bridges/go_redis_probe.go new file mode 100644 index 0000000000..9c8241c6d3 --- /dev/null +++ b/build/errors/bridges/go_redis_probe.go @@ -0,0 +1,89 @@ +// Runs one command, or one MULTI/EXEC block, through go-redis and prints what it returns or the error it gives. +// Arguments: port user password mode payload... (see _common.py for the format) +package main + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/redis/go-redis/v9" +) + +func tokens(p string) []interface{} { + var out []interface{} + for _, s := range strings.Split(p, "\x01") { + if strings.HasPrefix(s, "hex:") { + h := s[4:] + b := make([]byte, len(h)/2) + for i := range b { + fmt.Sscanf(h[2*i:2*i+2], "%02x", &b[i]) + } + out = append(out, b) + } else { + out = append(out, s) + } + } + return out +} + +func one(s string) string { return strings.NewReplacer("\r", " ", "\n", " ").Replace(s) } + +// The checks that go-redis offers for an error, and which of them say yes. +func checks(err error) string { + var yes []string + add := func(name string, ok bool) { + if ok { + yes = append(yes, name) + } + } + var e redis.Error + add("implements redis.Error", errors.As(err, &e)) + add("IsLoadingError", redis.IsLoadingError(err)) + add("IsReadOnlyError", redis.IsReadOnlyError(err)) + add("IsClusterDownError", redis.IsClusterDownError(err)) + add("IsTryAgainError", redis.IsTryAgainError(err)) + add("IsMasterDownError", redis.IsMasterDownError(err)) + add("IsMaxClientsError", redis.IsMaxClientsError(err)) + _, moved := redis.IsMovedError(err) + add("IsMovedError", moved) + _, ask := redis.IsAskError(err) + add("IsAskError", ask) + add("IsAuthError", redis.IsAuthError(err)) + add("IsPermissionError", redis.IsPermissionError(err)) + add("IsExecAbortError", redis.IsExecAbortError(err)) + add("IsOOMError", redis.IsOOMError(err)) + add("IsNoReplicasError", redis.IsNoReplicasError(err)) + add("== ErrNoScript", errors.Is(err, redis.ErrNoScript)) + add("== ErrCrossSlot", errors.Is(err, redis.ErrCrossSlot)) + return strings.Join(yes, ", ") +} + +func main() { + a := os.Args[1:] + c := redis.NewClient(&redis.Options{ + Addr: "127.0.0.1:" + a[0], Username: a[1], Password: a[2], + DialTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, + }) + defer c.Close() + ctx := context.Background() + var res interface{} + var err error + if a[3] == "cmd" { + res, err = c.Do(ctx, tokens(a[4])...).Result() + } else { + pipe := c.TxPipeline() + for _, p := range a[4:] { + pipe.Do(ctx, tokens(p)...) + } + res, err = pipe.Exec(ctx) + } + if err != nil { + fmt.Printf("EXC\t%T\t%s [checks: %s]\n", err, one(err.Error()), checks(err)) + return + } + fmt.Printf("OK\t%s\n", one(fmt.Sprint(res))) +} diff --git a/build/errors/bridges/lettuce.py b/build/errors/bridges/lettuce.py new file mode 100644 index 0000000000..286239dd97 --- /dev/null +++ b/build/errors/bridges/lettuce.py @@ -0,0 +1,34 @@ +"""Lettuce bridges for live_clients.py. Needs Java 17+ and Maven, and network access to Maven Central. + +The sync, async and reactive APIs are one library, so lettuce_sync.py, lettuce_async.py and +lettuce_reactive.py share this code and differ only in the API that the probe calls.""" +import os, subprocess +from pathlib import Path +from _common import parse, sh + +HERE = Path(__file__).parent +VERSION = '7.9.0.RELEASE' + +POM = '''4.0.0 +xprobe1 +io.lettucelettuce-core{v} +''' + + +def prepare_api(api, version, cache): + d = Path(cache) / 'bridges' / f'lettuce-{version}' + d.mkdir(parents=True, exist_ok=True) + env = {'JAVA_HOME': ''} + cp = d / 'cp.txt' + if not cp.exists(): + (d / 'pom.xml').write_text(POM.format(v=version)) + sh('mvn', '-q', '-B', 'dependency:build-classpath', f'-Dmdep.outputFile={cp}', cwd=d, env=env) + (d / 'LettuceProbe.java').write_text((HERE / 'LettuceProbe.java').read_text()) + sh('javac', '-cp', cp.read_text().strip(), 'LettuceProbe.java', cwd=d, env=env) + classpath = f'{cp.read_text().strip()}:{d}' + + def run(port, user, password, mode, *payload): + out = subprocess.run(['java', '-cp', classpath, 'LettuceProbe', api, str(port), user, password, mode, *payload], + capture_output=True, text=True, timeout=60, env={**os.environ, 'JAVA_HOME': ''}) + return parse(out.stdout, out.stderr) + return run diff --git a/build/errors/bridges/lettuce_async.py b/build/errors/bridges/lettuce_async.py new file mode 100644 index 0000000000..1c57a3a1b5 --- /dev/null +++ b/build/errors/bridges/lettuce_async.py @@ -0,0 +1,8 @@ +"""Lettuce (async API) bridge for live_clients.py.""" +from lettuce import VERSION, prepare_api + +KEY = 'lettuce_async' + + +def prepare(version, cache): + return prepare_api('async', version, cache) diff --git a/build/errors/bridges/lettuce_reactive.py b/build/errors/bridges/lettuce_reactive.py new file mode 100644 index 0000000000..2316f5e9a4 --- /dev/null +++ b/build/errors/bridges/lettuce_reactive.py @@ -0,0 +1,8 @@ +"""Lettuce (reactive API) bridge for live_clients.py.""" +from lettuce import VERSION, prepare_api + +KEY = 'lettuce_reactive' + + +def prepare(version, cache): + return prepare_api('reactive', version, cache) diff --git a/build/errors/bridges/lettuce_sync.py b/build/errors/bridges/lettuce_sync.py new file mode 100644 index 0000000000..465449cd47 --- /dev/null +++ b/build/errors/bridges/lettuce_sync.py @@ -0,0 +1,8 @@ +"""Lettuce (sync API) bridge for live_clients.py.""" +from lettuce import VERSION, prepare_api + +KEY = 'lettuce_sync' + + +def prepare(version, cache): + return prepare_api('sync', version, cache) diff --git a/build/errors/client_baselines/go_redis.json b/build/errors/client_baselines/go_redis.json new file mode 100644 index 0000000000..0b74cf94ff --- /dev/null +++ b/build/errors/client_baselines/go_redis.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "go_redis": "9.22.0" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "go_redis": [ + "proto.RedisError: ERR unknown command 'FOO', with args beginning with: 'bar' [checks: implements redis.Error]" + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "go_redis": [ + "proto.RedisError: ERR wrong number of arguments for 'get' command [checks: implements redis.Error]" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "go_redis": [ + "proto.RedisError: ERR syntax error [checks: implements redis.Error]" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "go_redis": [ + "proto.RedisError: ERR value is not an integer or out of range [checks: implements redis.Error]" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "go_redis": [ + "proto.RedisError: WRONGTYPE Operation against a key holding the wrong kind of value [checks: implements redis.Error]" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "go_redis": [ + "*proto.ExecAbortError: EXECABORT Transaction discarded because of previous errors. [checks: implements redis.Error, IsExecAbortError]" + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "go_redis": [ + "proto.RedisError: NOSCRIPT No matching script. Please use EVAL. [checks: implements redis.Error, == ErrNoScript]" + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "go_redis": [ + "proto.RedisError: BUSYKEY Target key name already exists. [checks: implements redis.Error]" + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "go_redis": [ + "proto.RedisError: BUSYGROUP Consumer Group name already exists [checks: implements redis.Error]" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "go_redis": [ + "proto.RedisError: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option [checks: implements redis.Error]" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "go_redis": [ + "proto.RedisError: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct? [checks: implements redis.Error]", + "*proto.AuthError: WRONGPASS invalid username-password pair or user is disabled. [checks: implements redis.Error, IsAuthError]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "go_redis": [ + "*proto.OOMError: OOM command not allowed when used memory > 'maxmemory'. [checks: implements redis.Error, IsOOMError]" + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "go_redis": [ + "proto.RedisError: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. [checks: implements redis.Error]" + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "go_redis": [ + "*errors.errorString: EOF [checks: ]" + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "go_redis": [ + "*proto.NoReplicasError: NOREPLICAS Not enough good replicas to write. [checks: implements redis.Error, IsNoReplicasError]" + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "go_redis": [ + "proto.RedisError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. [checks: implements redis.Error]" + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:1", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-107.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "go_redis": [ + "*proto.AuthError: NOAUTH Authentication required. [checks: implements redis.Error, IsAuthError]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "go_redis": [ + "*proto.PermissionError: NOPERM User app has no permissions to run the 'set' command [checks: implements redis.Error, IsPermissionError]", + "*proto.PermissionError: NOPERM No permissions to access a key [checks: implements redis.Error, IsPermissionError]" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "go_redis": [ + "*errors.errorString: EOF [checks: ]" + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "go_redis": [ + "*proto.ReadOnlyError: READONLY You can't write against a read only replica. [checks: implements redis.Error, IsReadOnlyError]" + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "go_redis": [ + "*proto.MasterDownError: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. [checks: implements redis.Error, IsMasterDownError]" + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "go_redis": [ + "*proto.LoadingError: LOADING Redis is loading the dataset in memory [checks: implements redis.Error, IsLoadingError]" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793715413", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2696816", + "loading_loaded_bytes:11255", + "loading_loaded_perc:1.69", + "loading_eta_seconds:1" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "go_redis": [ + "*proto.MovedError: MOVED 12182 127.0.0.1:6573 [checks: implements redis.Error, IsMovedError]" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "go_redis": [ + "proto.RedisError: CROSSSLOT Keys in request don't hash to the same slot [checks: implements redis.Error, == ErrCrossSlot]" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "go_redis": [ + "*proto.AskError: ASK 12182 127.0.0.1:6571 [checks: implements redis.Error, IsAskError]" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "go_redis": [ + "*proto.TryAgainError: TRYAGAIN Multiple keys request during rehashing of slot [checks: implements redis.Error, IsTryAgainError]" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "go_redis": [ + "*proto.ClusterDownError: CLUSTERDOWN Hash slot not served [checks: implements redis.Error, IsClusterDownError]", + "*proto.ClusterDownError: CLUSTERDOWN The cluster is down [checks: implements redis.Error, IsClusterDownError]" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/build/errors/client_baselines/lettuce_async.json b/build/errors/client_baselines/lettuce_async.json new file mode 100644 index 0000000000..4fc2b54403 --- /dev/null +++ b/build/errors/client_baselines/lettuce_async.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "lettuce_async": "7.9.0.RELEASE" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'FOO', with args beginning with: 'bar' [thrown as ExecutionException]" + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: ERR wrong number of arguments for 'get' command [thrown as ExecutionException]" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: ERR syntax error [thrown as ExecutionException]" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range [thrown as ExecutionException]" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: WRONGTYPE Operation against a key holding the wrong kind of value [thrown as ExecutionException]" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: EXECABORT Transaction discarded because of previous errors. [thrown as ExecutionException]" + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "lettuce_async": [ + "io.lettuce.core.RedisNoScriptException: NOSCRIPT No matching script. Please use EVAL. [thrown as ExecutionException]" + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "lettuce_async": [ + "io.lettuce.core.RedisBusyException: BUSYKEY Target key name already exists. [thrown as ExecutionException]" + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "lettuce_async": [ + "io.lettuce.core.RedisBusyException: BUSYGROUP Consumer Group name already exists [thrown as ExecutionException]" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option [thrown as ExecutionException]" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct? [thrown as ExecutionException]", + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6560 [cause io.lettuce.core.RedisCommandExecutionException: WRONGPASS invalid username-password pair or user is disabled.]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: OOM command not allowed when used memory > 'maxmemory'. [thrown as ExecutionException]" + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "lettuce_async": [ + "io.lettuce.core.RedisBusyException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. [thrown as ExecutionException]" + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "lettuce_async": [ + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6557 [cause io.lettuce.core.RedisCommandExecutionException: ERR max number of clients reached]" + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: NOREPLICAS Not enough good replicas to write. [thrown as ExecutionException]" + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. [thrown as ExecutionException]" + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-106.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "lettuce_async": [ + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6560 [cause io.lettuce.core.RedisCommandExecutionException: NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: NOPERM User app has no permissions to run the 'set' command [thrown as ExecutionException]", + "io.lettuce.core.RedisCommandExecutionException: NOPERM No permissions to access a key [thrown as ExecutionException]" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "lettuce_async": [ + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6565 [cause io.lettuce.core.RedisConnectionException: DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside.]" + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "lettuce_async": [ + "io.lettuce.core.RedisReadOnlyException: READONLY You can't write against a read only replica. [thrown as ExecutionException]" + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. [thrown as ExecutionException]" + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "lettuce_async": [ + "io.lettuce.core.RedisLoadingException: LOADING Redis is loading the dataset in memory [thrown as ExecutionException]" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793715321", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2696464", + "loading_loaded_bytes:30709", + "loading_loaded_perc:4.60", + "loading_eta_seconds:41" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: MOVED 12182 127.0.0.1:6573 [thrown as ExecutionException]" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: CROSSSLOT Keys in request don't hash to the same slot [thrown as ExecutionException]" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: ASK 12182 127.0.0.1:6571 [thrown as ExecutionException]" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: TRYAGAIN Multiple keys request during rehashing of slot [thrown as ExecutionException]" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "lettuce_async": [ + "io.lettuce.core.RedisCommandExecutionException: CLUSTERDOWN Hash slot not served [thrown as ExecutionException]", + "io.lettuce.core.RedisCommandExecutionException: CLUSTERDOWN The cluster is down [thrown as ExecutionException]" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/build/errors/client_baselines/lettuce_reactive.json b/build/errors/client_baselines/lettuce_reactive.json new file mode 100644 index 0000000000..fbf0b74c2e --- /dev/null +++ b/build/errors/client_baselines/lettuce_reactive.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "lettuce_reactive": "7.9.0.RELEASE" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'FOO', with args beginning with: 'bar' " + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: ERR wrong number of arguments for 'get' command" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: ERR syntax error" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: WRONGTYPE Operation against a key holding the wrong kind of value" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: EXECABORT Transaction discarded because of previous errors." + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisNoScriptException: NOSCRIPT No matching script. Please use EVAL." + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisBusyException: BUSYKEY Target key name already exists." + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisBusyException: BUSYGROUP Consumer Group name already exists" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6560 [cause io.lettuce.core.RedisCommandExecutionException: WRONGPASS invalid username-password pair or user is disabled.]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: OOM command not allowed when used memory > 'maxmemory'." + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisBusyException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE." + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6557 [cause io.lettuce.core.RedisCommandExecutionException: ERR max number of clients reached]" + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: NOREPLICAS Not enough good replicas to write." + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-107.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6560 [cause io.lettuce.core.RedisCommandExecutionException: NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: NOPERM User app has no permissions to run the 'set' command", + "io.lettuce.core.RedisCommandExecutionException: NOPERM No permissions to access a key" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6565 [cause io.lettuce.core.RedisConnectionException: DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside.]" + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisReadOnlyException: READONLY You can't write against a read only replica." + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisLoadingException: LOADING Redis is loading the dataset in memory" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793715379", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2695936", + "loading_loaded_bytes:28666", + "loading_loaded_perc:4.29", + "loading_eta_seconds:44" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: MOVED 12182 127.0.0.1:6573" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: CROSSSLOT Keys in request don't hash to the same slot" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: ASK 12182 127.0.0.1:6571" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: TRYAGAIN Multiple keys request during rehashing of slot" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "lettuce_reactive": [ + "io.lettuce.core.RedisCommandExecutionException: CLUSTERDOWN Hash slot not served", + "io.lettuce.core.RedisCommandExecutionException: CLUSTERDOWN The cluster is down" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/build/errors/client_baselines/lettuce_sync.json b/build/errors/client_baselines/lettuce_sync.json new file mode 100644 index 0000000000..3d7a2a7e72 --- /dev/null +++ b/build/errors/client_baselines/lettuce_sync.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "lettuce_sync": "7.9.0.RELEASE" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'FOO', with args beginning with: 'bar' [cause io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'FOO', with args beginning with: 'bar' ]" + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: ERR wrong number of arguments for 'get' command [cause io.lettuce.core.RedisCommandExecutionException: ERR wrong number of arguments for 'get' command]" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: ERR syntax error [cause io.lettuce.core.RedisCommandExecutionException: ERR syntax error]" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range [cause io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range]" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: WRONGTYPE Operation against a key holding the wrong kind of value [cause io.lettuce.core.RedisCommandExecutionException: WRONGTYPE Operation against a key holding the wrong kind of value]" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: EXECABORT Transaction discarded because of previous errors. [cause io.lettuce.core.RedisCommandExecutionException: EXECABORT Transaction discarded because of previous errors.]" + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisNoScriptException: NOSCRIPT No matching script. Please use EVAL. [cause io.lettuce.core.RedisNoScriptException: NOSCRIPT No matching script. Please use EVAL.]" + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisBusyException: BUSYKEY Target key name already exists. [cause io.lettuce.core.RedisBusyException: BUSYKEY Target key name already exists.]" + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisBusyException: BUSYGROUP Consumer Group name already exists [cause io.lettuce.core.RedisBusyException: BUSYGROUP Consumer Group name already exists]" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option [cause io.lettuce.core.RedisCommandExecutionException: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option]" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct? [cause io.lettuce.core.RedisCommandExecutionException: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?]", + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6560 [cause io.lettuce.core.RedisCommandExecutionException: WRONGPASS invalid username-password pair or user is disabled.]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: OOM command not allowed when used memory > 'maxmemory'. [cause io.lettuce.core.RedisCommandExecutionException: OOM command not allowed when used memory > 'maxmemory'.]" + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisBusyException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. [cause io.lettuce.core.RedisBusyException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.]" + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6557 [cause io.lettuce.core.RedisCommandExecutionException: ERR max number of clients reached]" + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: NOREPLICAS Not enough good replicas to write. [cause io.lettuce.core.RedisCommandExecutionException: NOREPLICAS Not enough good replicas to write.]" + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. [cause io.lettuce.core.RedisCommandExecutionException: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.]" + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-106.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6560 [cause io.lettuce.core.RedisCommandExecutionException: NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: NOPERM User app has no permissions to run the 'set' command [cause io.lettuce.core.RedisCommandExecutionException: NOPERM User app has no permissions to run the 'set' command]", + "io.lettuce.core.RedisCommandExecutionException: NOPERM No permissions to access a key [cause io.lettuce.core.RedisCommandExecutionException: NOPERM No permissions to access a key]" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1/:6565 [cause io.lettuce.core.RedisConnectionException: DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside.]" + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisReadOnlyException: READONLY You can't write against a read only replica. [cause io.lettuce.core.RedisReadOnlyException: READONLY You can't write against a read only replica.]" + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. [cause io.lettuce.core.RedisCommandExecutionException: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.]" + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisLoadingException: LOADING Redis is loading the dataset in memory [cause io.lettuce.core.RedisLoadingException: LOADING Redis is loading the dataset in memory]" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793715265", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2694416", + "loading_loaded_bytes:29685", + "loading_loaded_perc:4.44", + "loading_eta_seconds:21" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: MOVED 12182 127.0.0.1:6573 [cause io.lettuce.core.RedisCommandExecutionException: MOVED 12182 127.0.0.1:6573]" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: CROSSSLOT Keys in request don't hash to the same slot [cause io.lettuce.core.RedisCommandExecutionException: CROSSSLOT Keys in request don't hash to the same slot]" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: ASK 12182 127.0.0.1:6571 [cause io.lettuce.core.RedisCommandExecutionException: ASK 12182 127.0.0.1:6571]" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: TRYAGAIN Multiple keys request during rehashing of slot [cause io.lettuce.core.RedisCommandExecutionException: TRYAGAIN Multiple keys request during rehashing of slot]" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "lettuce_sync": [ + "io.lettuce.core.RedisCommandExecutionException: CLUSTERDOWN Hash slot not served [cause io.lettuce.core.RedisCommandExecutionException: CLUSTERDOWN Hash slot not served]", + "io.lettuce.core.RedisCommandExecutionException: CLUSTERDOWN The cluster is down [cause io.lettuce.core.RedisCommandExecutionException: CLUSTERDOWN The cluster is down]" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/content/develop/reference/errors/common/loading.md b/content/develop/reference/errors/common/loading.md index 6edf8fa2e1..c29656c4bc 100644 --- a/content/develop/reference/errors/common/loading.md +++ b/content/develop/reference/errors/common/loading.md @@ -19,6 +19,7 @@ error: match: - Redis is loading the dataset in memory - BusyLoadingError + - RedisLoadingException --- Redis returns this error while it's loading your data into memory. This happens when the server starts and reads its data files from disk. It can also happen on a replica while it loads a full copy of the data from its primary. Most commands fail until loading finishes. diff --git a/content/develop/reference/errors/common/noscript.md b/content/develop/reference/errors/common/noscript.md index 835b8d15a5..4713e274f9 100644 --- a/content/develop/reference/errors/common/noscript.md +++ b/content/develop/reference/errors/common/noscript.md @@ -20,6 +20,7 @@ error: - No matching script - NoScriptError - JedisNoScriptException + - RedisNoScriptException --- Redis returns this error when you run a Lua script with [`EVALSHA`](/content/commands/evalsha.md), but the server doesn't have the script in its script cache. `EVALSHA` runs a script by its SHA1 (Secure Hash Algorithm 1) digest, a short code calculated from the script's source. The server only knows the digest of a script that it has already loaded. diff --git a/content/develop/reference/errors/common/readonly.md b/content/develop/reference/errors/common/readonly.md index 27c14cbe0f..784e4ac1cd 100644 --- a/content/develop/reference/errors/common/readonly.md +++ b/content/develop/reference/errors/common/readonly.md @@ -20,6 +20,7 @@ error: - You can't write against a read only replica - Can not run script with write flag on readonly replica - ReadOnlyError + - RedisReadOnlyException --- Redis returns this error when you send a command that changes data to a replica. A replica keeps a copy of a primary's data, and by default it's read-only. Only the primary accepts writes. diff --git a/data/client-exceptions.json b/data/client-exceptions.json index 676e65f1b9..a3f5fa7910 100644 --- a/data/client-exceptions.json +++ b/data/client-exceptions.json @@ -3,12 +3,16 @@ "versions": { "redis-py": "7.4.1", "jedis": "8.0.2", - "node-redis": "6.3.1" + "node-redis": "6.3.1", + "lettuce": "7.9.0.RELEASE", + "go-redis": "9.22.0" }, "order": [ "redis-py", "jedis", - "node-redis" + "node-redis", + "lettuce", + "go-redis" ], "clients": { "redis-py": { @@ -28,6 +32,19 @@ "package": "redis", "generic": "SimpleError", "basis": "live" + }, + "lettuce": { + "name": "Lettuce", + "package": "io.lettuce.core", + "generic": "RedisCommandExecutionException", + "basis": "live", + "note": "The async API wraps the exception in an `ExecutionException` (`get()`) or a `CompletionException` (`join()`)." + }, + "go-redis": { + "name": "go-redis", + "package": "github.com/redis/go-redis/v9", + "generic": "redis.Error", + "basis": "live" } }, "errors": { @@ -45,7 +62,19 @@ "node-redis": { "class": "SimpleError", "message": "ASK :", - "note": "`createCluster` follows the redirect itself, and throws `MaxCommandRedirectionsError` after 16 attempts by default." + "note": "`createCluster` follows the redirect itself, and throws `MaxCommandRedirectionsError` after 16 attempts by default.", + "check": "err.message.startsWith('ASK')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", + "message": "ASK :", + "note": "`RedisClusterClient` follows the redirect itself, and raises this error after 5 redirects by default." + }, + "go-redis": { + "class": "redis.Error", + "message": "ASK :", + "check": "redis.IsAskError(err)", + "note": "`ClusterClient` follows the redirect itself, and returns this error after `MaxRedirects` attempts (3 by default)." } }, "busy": { @@ -59,7 +88,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "check": "err.message.startsWith('BUSY')" + }, + "lettuce": { + "class": "RedisBusyException", "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE." + }, + "go-redis": { + "class": "redis.Error", + "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "check": "redis.HasErrorPrefix(err, \"BUSY\")" } }, "busygroup": { @@ -73,7 +112,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "BUSYGROUP Consumer Group name already exists", + "check": "err.message.startsWith('BUSYGROUP')" + }, + "lettuce": { + "class": "RedisBusyException", "message": "BUSYGROUP Consumer Group name already exists" + }, + "go-redis": { + "class": "redis.Error", + "message": "BUSYGROUP Consumer Group name already exists", + "check": "redis.HasErrorPrefix(err, \"BUSYGROUP\")" } }, "busykey": { @@ -87,7 +136,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "BUSYKEY Target key name already exists.", + "check": "err.message.startsWith('BUSYKEY')" + }, + "lettuce": { + "class": "RedisBusyException", "message": "BUSYKEY Target key name already exists." + }, + "go-redis": { + "class": "redis.Error", + "message": "BUSYKEY Target key name already exists.", + "check": "redis.HasErrorPrefix(err, \"BUSYKEY\")" } }, "clusterdown": { @@ -101,7 +160,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "CLUSTERDOWN The cluster is down", + "check": "err.message.startsWith('CLUSTERDOWN')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "CLUSTERDOWN The cluster is down" + }, + "go-redis": { + "class": "redis.Error", + "message": "CLUSTERDOWN The cluster is down", + "check": "redis.IsClusterDownError(err)" } }, "crossslot": { @@ -115,7 +184,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "CROSSSLOT Keys in request don't hash to the same slot", + "check": "err.message.startsWith('CROSSSLOT')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "CROSSSLOT Keys in request don't hash to the same slot" + }, + "go-redis": { + "class": "redis.Error", + "message": "CROSSSLOT Keys in request don't hash to the same slot", + "check": "errors.Is(err, redis.ErrCrossSlot)" } }, "denied": { @@ -129,7 +208,18 @@ }, "node-redis": { "class": "SimpleError", - "message": "DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. ..." + "message": "DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. ...", + "check": "err.message.startsWith('DENIED')" + }, + "lettuce": { + "class": "RedisConnectionException", + "message": "Unable to connect to /:", + "note": "The cause is a `RedisConnectionException` with the `DENIED` message." + }, + "go-redis": { + "class": "io.EOF", + "message": "EOF", + "note": "The server closes the connection after it sends the error, and go-redis reported `EOF` in our run, not the error text." } }, "err-max-clients": { @@ -144,6 +234,16 @@ "node-redis": { "class": "SimpleError", "message": "ERR max number of clients reached" + }, + "lettuce": { + "class": "RedisConnectionException", + "message": "Unable to connect to /:", + "note": "The cause is a `RedisCommandExecutionException` with the message `ERR max number of clients reached`." + }, + "go-redis": { + "class": "io.EOF", + "message": "EOF", + "note": "The server closes the connection after it sends the error, and go-redis reported `EOF` in our run, not the error text." } }, "err-not-an-integer": { @@ -158,6 +258,14 @@ "node-redis": { "class": "SimpleError", "message": "ERR value is not an integer or out of range" + }, + "lettuce": { + "class": "RedisCommandExecutionException", + "message": "ERR value is not an integer or out of range" + }, + "go-redis": { + "class": "redis.Error", + "message": "ERR value is not an integer or out of range" } }, "err-syntax-error": { @@ -172,6 +280,14 @@ "node-redis": { "class": "SimpleError", "message": "ERR syntax error" + }, + "lettuce": { + "class": "RedisCommandExecutionException", + "message": "ERR syntax error" + }, + "go-redis": { + "class": "redis.Error", + "message": "ERR syntax error" } }, "err-unknown-command": { @@ -186,6 +302,14 @@ "node-redis": { "class": "SimpleError", "message": "ERR unknown command '', with args beginning with: " + }, + "lettuce": { + "class": "RedisCommandExecutionException", + "message": "ERR unknown command '', with args beginning with: " + }, + "go-redis": { + "class": "redis.Error", + "message": "ERR unknown command '', with args beginning with: " } }, "err-wrong-number-of-arguments": { @@ -200,6 +324,14 @@ "node-redis": { "class": "SimpleError", "message": "ERR wrong number of arguments for '' command" + }, + "lettuce": { + "class": "RedisCommandExecutionException", + "message": "ERR wrong number of arguments for '' command" + }, + "go-redis": { + "class": "redis.Error", + "message": "ERR wrong number of arguments for '' command" } }, "execabort": { @@ -217,6 +349,15 @@ "class": "SimpleError", "message": "ERR unknown command ''", "note": "`exec()` rejects with the error of the command that failed to queue, not `EXECABORT`." + }, + "lettuce": { + "class": "RedisCommandExecutionException", + "message": "EXECABORT Transaction discarded because of previous errors." + }, + "go-redis": { + "class": "redis.Error", + "message": "EXECABORT Transaction discarded because of previous errors.", + "check": "redis.IsExecAbortError(err)" } }, "loading": { @@ -230,7 +371,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "LOADING Redis is loading the dataset in memory", + "check": "err.message.startsWith('LOADING')" + }, + "lettuce": { + "class": "RedisLoadingException", "message": "LOADING Redis is loading the dataset in memory" + }, + "go-redis": { + "class": "redis.Error", + "message": "LOADING Redis is loading the dataset in memory", + "check": "redis.IsLoadingError(err)" } }, "masterdown": { @@ -244,7 +395,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.", + "check": "err.message.startsWith('MASTERDOWN')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + }, + "go-redis": { + "class": "redis.Error", + "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.", + "check": "redis.IsMasterDownError(err)" } }, "misconf": { @@ -258,7 +419,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.", + "check": "err.message.startsWith('MISCONF')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + }, + "go-redis": { + "class": "redis.Error", + "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.", + "check": "redis.HasErrorPrefix(err, \"MISCONF\")" } }, "moved": { @@ -275,7 +446,19 @@ "node-redis": { "class": "SimpleError", "message": "MOVED :", - "note": "`createCluster` follows the redirect itself, and throws `MaxCommandRedirectionsError` after 16 attempts by default." + "note": "`createCluster` follows the redirect itself, and throws `MaxCommandRedirectionsError` after 16 attempts by default.", + "check": "err.message.startsWith('MOVED')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", + "message": "MOVED :", + "note": "`RedisClusterClient` follows the redirect itself, and raises this error after 5 redirects by default." + }, + "go-redis": { + "class": "redis.Error", + "message": "MOVED :", + "check": "redis.IsMovedError(err)", + "note": "`ClusterClient` follows the redirect itself, and returns this error after `MaxRedirects` attempts (3 by default)." } }, "noauth": { @@ -290,7 +473,18 @@ "node-redis": { "class": "SimpleError", "message": "NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time", - "note": "node-redis sends `HELLO` when it connects, so `connect()` raises this error, and the message names `HELLO`." + "note": "node-redis sends `HELLO` when it connects, so `connect()` raises this error, and the message names `HELLO`.", + "check": "err.message.startsWith('NOAUTH')" + }, + "lettuce": { + "class": "RedisConnectionException", + "message": "Unable to connect to /:", + "note": "The cause is a `RedisCommandExecutionException` with the message `NOAUTH HELLO must be called with the client already authenticated\u2026`. Lettuce authenticates and sends `HELLO` when it connects, so `connect()` raises this error." + }, + "go-redis": { + "class": "redis.Error", + "message": "NOAUTH Authentication required.", + "check": "redis.IsAuthError(err)" } }, "nogroup": { @@ -304,7 +498,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option", + "check": "err.message.startsWith('NOGROUP')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option" + }, + "go-redis": { + "class": "redis.Error", + "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option", + "check": "redis.HasErrorPrefix(err, \"NOGROUP\")" } }, "noperm": { @@ -318,7 +522,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "NOPERM User has no permissions to run the '' command", + "check": "err.message.startsWith('NOPERM')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "NOPERM User has no permissions to run the '' command" + }, + "go-redis": { + "class": "redis.Error", + "message": "NOPERM User has no permissions to run the '' command", + "check": "redis.IsPermissionError(err)" } }, "noreplicas": { @@ -332,7 +546,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "NOREPLICAS Not enough good replicas to write.", + "check": "err.message.startsWith('NOREPLICAS')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "NOREPLICAS Not enough good replicas to write." + }, + "go-redis": { + "class": "redis.Error", + "message": "NOREPLICAS Not enough good replicas to write.", + "check": "redis.IsNoReplicasError(err)" } }, "noscript": { @@ -346,7 +570,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "NOSCRIPT No matching script. Please use EVAL.", + "check": "err.message.startsWith('NOSCRIPT')" + }, + "lettuce": { + "class": "RedisNoScriptException", "message": "NOSCRIPT No matching script. Please use EVAL." + }, + "go-redis": { + "class": "redis.Error", + "message": "NOSCRIPT No matching script. Please use EVAL.", + "check": "errors.Is(err, redis.ErrNoScript)" } }, "oom": { @@ -360,7 +594,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "OOM command not allowed when used memory > 'maxmemory'.", + "check": "err.message.startsWith('OOM')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "OOM command not allowed when used memory > 'maxmemory'." + }, + "go-redis": { + "class": "redis.Error", + "message": "OOM command not allowed when used memory > 'maxmemory'.", + "check": "redis.IsOOMError(err)" } }, "readonly": { @@ -374,7 +618,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "READONLY You can't write against a read only replica.", + "check": "err.message.startsWith('READONLY')" + }, + "lettuce": { + "class": "RedisReadOnlyException", "message": "READONLY You can't write against a read only replica." + }, + "go-redis": { + "class": "redis.Error", + "message": "READONLY You can't write against a read only replica.", + "check": "redis.IsReadOnlyError(err)" } }, "tryagain": { @@ -388,7 +642,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "TRYAGAIN Multiple keys request during rehashing of slot", + "check": "err.message.startsWith('TRYAGAIN')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "TRYAGAIN Multiple keys request during rehashing of slot" + }, + "go-redis": { + "class": "redis.Error", + "message": "TRYAGAIN Multiple keys request during rehashing of slot", + "check": "redis.IsTryAgainError(err)" } }, "wrongpass": { @@ -403,7 +667,19 @@ }, "node-redis": { "class": "SimpleError", - "message": "WRONGPASS invalid username-password pair or user is disabled." + "message": "WRONGPASS invalid username-password pair or user is disabled.", + "check": "err.message.startsWith('WRONGPASS')" + }, + "lettuce": { + "class": "RedisConnectionException", + "message": "Unable to connect to /:", + "note": "The cause is a `RedisCommandExecutionException` with the server's `WRONGPASS` message. `AUTH` with a password for a user that has none raises `RedisCommandExecutionException` with the `ERR AUTH called without any password configured\u2026` message, and no `RedisConnectionException`." + }, + "go-redis": { + "class": "redis.Error", + "message": "WRONGPASS invalid username-password pair or user is disabled.", + "check": "redis.IsAuthError(err)", + "note": "`AUTH` with a password for a user that has none returns `ERR AUTH called without any password configured\u2026`, which has no check of its own." } }, "wrongtype": { @@ -417,7 +693,17 @@ }, "node-redis": { "class": "SimpleError", + "message": "WRONGTYPE Operation against a key holding the wrong kind of value", + "check": "err.message.startsWith('WRONGTYPE')" + }, + "lettuce": { + "class": "RedisCommandExecutionException", "message": "WRONGTYPE Operation against a key holding the wrong kind of value" + }, + "go-redis": { + "class": "redis.Error", + "message": "WRONGTYPE Operation against a key holding the wrong kind of value", + "check": "redis.HasErrorPrefix(err, \"WRONGTYPE\")" } } } diff --git a/layouts/partials/errors/client-rows.html b/layouts/partials/errors/client-rows.html index 6413ce8e0e..2b018d6d43 100644 --- a/layouts/partials/errors/client-rows.html +++ b/layouts/partials/errors/client-rows.html @@ -5,7 +5,7 @@ Input: the page's file name without the extension, such as "readonly". Returns a slice of dicts, one per client, in the order of "order" in data/client-exceptions.json: - client, package, class, message, note. Empty when the page has no entry. + client, clientNote, package, class, check, message, note. Empty when the page has no entry. */ -}} {{- $data := index site.Data "client-exceptions" -}} {{- $rows := slice -}} @@ -16,8 +16,10 @@ {{- with index $entry $id -}} {{- $rows = $rows | append (dict "client" $client.name + "clientNote" ($client.note | default "") "package" $client.package "class" .class + "check" (.check | default "") "message" .message "note" (.note | default "")) -}} {{- end -}} diff --git a/layouts/partials/markdown-error-clients.html b/layouts/partials/markdown-error-clients.html index 48660fcc32..0f79ac26c5 100644 --- a/layouts/partials/markdown-error-clients.html +++ b/layouts/partials/markdown-error-clients.html @@ -22,12 +22,16 @@ {{- end -}} {{- range $match := $matches -}} - {{- $md := "| Client library | Exception | Message |\n|---|---|---|\n" -}} + {{- $md := "| Client library | Exception or error type | Message |\n|---|---|---|\n" -}} {{- range $rows -}} {{- $msg := replace .message "|" "\\|" -}} {{- $msg = cond (in $msg "`") (printf "`` %s ``" $msg) (printf "`%s`" $msg) -}} {{- with .note -}}{{- $msg = printf "%s %s" $msg . -}}{{- end -}} - {{- $md = printf "%s| %s | `%s` | %s |\n" $md .client .class $msg -}} + {{- $type := printf "`%s`" .class -}} + {{- with .check -}}{{- $type = printf "%s, test with `%s`" $type . -}}{{- end -}} + {{- $client := .client -}} + {{- with .clientNote -}}{{- $client = printf "%s. %s" $client . -}}{{- end -}} + {{- $md = printf "%s| %s | %s | %s |\n" $md $client $type $msg -}} {{- end -}} {{- $content = replace $content $match $md 1 -}} {{- end -}} diff --git a/layouts/shortcodes/error-clients.html b/layouts/shortcodes/error-clients.html index 4da7afd889..b8d6151adf 100644 --- a/layouts/shortcodes/error-clients.html +++ b/layouts/shortcodes/error-clients.html @@ -16,12 +16,12 @@
- + {{- range $rows }} - - + + {{- end }} From 4e8e3dcd3ded8547ad6a9f303a631c7e5520ff65 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Tue, 3 Nov 2026 15:16:27 +0000 Subject: [PATCH 05/11] DOC-7253 Add ioredis, StackExchange.Redis, Predis, redis-rb and redis-rs to the client exceptions Runs five more clients through the live checks (ioredis 5.11.1, StackExchange.Redis 3.0.0, Predis 3.5.1, redis-rb 6.0.0, redis-rs 1.6.0), each with a bridge and probe in build/errors/bridges and its result in client_baselines. The table on each common page now covers ten clients. StackExchange.Redis stands for NRedisStack too, because NRedisStack 1.6.0 depends on it. hiredis (a C library with no exceptions) and RedisVL (it wraps redis-py) are not covered. The clients fall into three groups, and the table shows which. redis-py, Jedis, Lettuce, redis-rb and redis-rs turn some prefixes into their own classes (redis-rs has a real ErrorKind enum, and the rest are partial). ioredis, node-redis and Predis keep one class and expose the prefix through the message, or for Predis through getErrorType(), so those rows carry a check. go-redis uses helper functions. Predis was first wrong because of my probe, not the client: RawCommand takes the command name and the arguments separately. Several rows are not what the server's text suggests. StackExchange.Redis raises RedisConnectionException, not a server exception, whenever the handshake fails, so NOAUTH, WRONGPASS, DENIED, max-clients, BUSY and NOPERM (for the restricted user in the check) come out of Connect. It refuses a write to a replica itself, and it follows MOVED so nothing reaches the caller. redis-rb and Lettuce send HELLO when they connect, so NOAUTH there names HELLO, as in node-redis. ioredis never raises LOADING by default, because its ready check waits for the load to finish (confirmed in Redis.js and live: with enableReadyCheck set to false it raises ReplyError). redis-rb, Predis and node-redis raise the error of the command that failed to queue and not EXECABORT, like redis-py, while redis-rs and ioredis do report it. The ASK row is left out for StackExchange.Redis because the sync call raised a connection error about the hash slot and the async call raised nothing, in a deliberately half-migrated slot. I could not tell which a user would see. Learned: how a client surfaces a server error depends as much on its connect handshake and its retry or ready-check behavior as on its error classes, so the same page can show a server error in one client and a connection error in another Constraint: the ioredis loading row only holds with enableReadyCheck set to false; the default behavior is to wait Directive: Keep ASK out of the StackExchange.Redis column unless it is re-checked on a real cluster rather than the half-migrated slot of live_checks.py Gaps: StackExchange.Redis NOPERM and BUSY as RedisConnectionException come from a user and a script that blocked the handshake; a user that can run the handshake commands was not checked Gaps: redis-rb cluster, redis-rs cluster and Predis cluster redirect behavior was not checked Recheck: when StackExchange.Redis, redis-rb or ioredis change their connect handshake or ready check Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- build/errors/README.md | 7 +- build/errors/bridges/SeRedisProbe.cs | 34 + build/errors/bridges/ioredis.py | 23 + build/errors/bridges/ioredis_probe.mjs | 35 + build/errors/bridges/predis.py | 22 + build/errors/bridges/predis_probe.php | 30 + build/errors/bridges/redis_rb.py | 23 + build/errors/bridges/redis_rb_probe.rb | 23 + build/errors/bridges/rust_async.py | 8 + build/errors/bridges/rust_redis.py | 32 + build/errors/bridges/rust_redis_src/main.rs | 61 ++ build/errors/bridges/rust_sync.py | 8 + build/errors/bridges/se_redis.py | 28 + build/errors/bridges/se_redis_async.py | 8 + build/errors/bridges/se_redis_sync.py | 8 + build/errors/client_baselines/ioredis.json | 368 ++++++++++ build/errors/client_baselines/predis.json | 367 ++++++++++ build/errors/client_baselines/redis_rb.json | 367 ++++++++++ build/errors/client_baselines/rust_async.json | 367 ++++++++++ build/errors/client_baselines/rust_sync.json | 367 ++++++++++ .../client_baselines/se_redis_async.json | 367 ++++++++++ .../client_baselines/se_redis_sync.json | 367 ++++++++++ .../reference/errors/common/wrongtype.md | 1 + data/client-exceptions.json | 659 +++++++++++++++++- layouts/partials/markdown-error-clients.html | 4 +- layouts/shortcodes/error-clients.html | 3 +- 26 files changed, 3576 insertions(+), 11 deletions(-) create mode 100644 build/errors/bridges/SeRedisProbe.cs create mode 100644 build/errors/bridges/ioredis.py create mode 100644 build/errors/bridges/ioredis_probe.mjs create mode 100644 build/errors/bridges/predis.py create mode 100644 build/errors/bridges/predis_probe.php create mode 100644 build/errors/bridges/redis_rb.py create mode 100644 build/errors/bridges/redis_rb_probe.rb create mode 100644 build/errors/bridges/rust_async.py create mode 100644 build/errors/bridges/rust_redis.py create mode 100644 build/errors/bridges/rust_redis_src/main.rs create mode 100644 build/errors/bridges/rust_sync.py create mode 100644 build/errors/bridges/se_redis.py create mode 100644 build/errors/bridges/se_redis_async.py create mode 100644 build/errors/bridges/se_redis_sync.py create mode 100644 build/errors/client_baselines/ioredis.json create mode 100644 build/errors/client_baselines/predis.json create mode 100644 build/errors/client_baselines/redis_rb.json create mode 100644 build/errors/client_baselines/rust_async.json create mode 100644 build/errors/client_baselines/rust_sync.json create mode 100644 build/errors/client_baselines/se_redis_async.json create mode 100644 build/errors/client_baselines/se_redis_sync.json diff --git a/build/errors/README.md b/build/errors/README.md index 9166176580..72f6a41331 100644 --- a/build/errors/README.md +++ b/build/errors/README.md @@ -122,8 +122,11 @@ python3 build/errors/live_clients.py jedis python3 build/errors/live_clients.py node_redis --only standalone ``` -It needs Docker, like `verify`, and the client's tools: Java and Maven for Jedis, -Node.js and npm for node-redis. Each client is a bridge in `bridges/`: a short +It needs Docker, like `verify`, and the client's tools: Java and Maven (`jedis`, +`lettuce_sync`, `lettuce_async`, `lettuce_reactive`), Node.js and npm +(`node_redis`, `ioredis`), Go (`go_redis`), the .NET SDK (`se_redis_sync`, +`se_redis_async`), PHP and Composer (`predis`), Ruby (`redis_rb`), and Rust +(`rust_sync`, `rust_async`). Each client is a bridge in `bridges/`: a short Python module and a probe program in the client's language that runs one command and prints the exception class and message. The run writes `client_baselines/.json`. Copy the classes and messages that the pages diff --git a/build/errors/bridges/SeRedisProbe.cs b/build/errors/bridges/SeRedisProbe.cs new file mode 100644 index 0000000000..02611bf394 --- /dev/null +++ b/build/errors/bridges/SeRedisProbe.cs @@ -0,0 +1,34 @@ +// Runs one command, or one MULTI/EXEC block, through StackExchange.Redis and prints what it returns or throws. +// Arguments: api port user password mode payload... (see _common.py for the format) +using StackExchange.Redis; + +string api = args[0]; int port = int.Parse(args[1]); string user = args[2], pass = args[3], mode = args[4]; +string One(string s) => (s ?? "").Replace("\r", " ").Replace("\n", " "); +object[] Tokens(string p) => p.Split('\u0001').Skip(1).Select(s => s.StartsWith("hex:") + ? (object)Convert.FromHexString(s.Substring(4)) : s).ToArray(); +string Name(string p) => p.Split('\u0001')[0]; +string Describe(Exception e) => + $"EXC\t{e.GetType().FullName}\t{One(e.Message)}" + (e.InnerException != null ? $" [inner {e.InnerException.GetType().FullName}: {One(e.InnerException.Message)}]" : ""); + +var o = new ConfigurationOptions { ConnectTimeout = 5000, SyncTimeout = 5000, AsyncTimeout = 5000, AbortOnConnectFail = true }; +o.EndPoints.Add("127.0.0.1", port); +if (user != "") o.User = user; +if (pass != "") o.Password = pass; +try { + using var mux = api == "sync" ? ConnectionMultiplexer.Connect(o) : await ConnectionMultiplexer.ConnectAsync(o); + var db = mux.GetDatabase(); + if (mode == "cmd") { + var p = args[5]; + var r = api == "sync" ? db.Execute(Name(p), Tokens(p)) : await db.ExecuteAsync(Name(p), Tokens(p)); + Console.WriteLine("OK\t" + One(r.ToString())); + } else { + var tx = db.CreateTransaction(); + var tasks = args.Skip(5).Select(p => tx.ExecuteAsync(Name(p), Tokens(p))).ToList(); + bool committed = api == "sync" ? tx.Execute() : await tx.ExecuteAsync(); + var errs = new List(); + foreach (var t in tasks) { try { await t; } catch (Exception e) { errs.Add(e.GetType().Name + ": " + One(e.Message)); } } + Console.WriteLine($"OK\tcommitted={committed}" + (errs.Count > 0 ? " [command errors: " + string.Join("; ", errs) + "]" : "")); + } +} catch (Exception e) { + Console.WriteLine(Describe(e)); +} diff --git a/build/errors/bridges/ioredis.py b/build/errors/bridges/ioredis.py new file mode 100644 index 0000000000..93cbd557bd --- /dev/null +++ b/build/errors/bridges/ioredis.py @@ -0,0 +1,23 @@ +"""ioredis bridge for live_clients.py. Needs Node.js 18+ and npm, and network access to the npm registry.""" +import os, subprocess +from pathlib import Path +from _common import parse, sh + +KEY = 'ioredis' +VERSION = '5.11.1' +HERE = Path(__file__).parent + + +def prepare(version, cache): + d = Path(cache) / 'bridges' / f'ioredis-{version}' + d.mkdir(parents=True, exist_ok=True) + if not (d / 'node_modules' / 'ioredis').exists(): + (d / 'package.json').write_text('{"type":"module"}') + sh('npm', 'install', '--no-audit', '--no-fund', '--cache', str(d / '.npm-cache'), f'ioredis@{version}', cwd=d) + (d / 'probe.mjs').write_text((HERE / 'ioredis_probe.mjs').read_text()) + + def run(port, user, password, mode, *payload): + out = subprocess.run(['node', 'probe.mjs', str(port), user, password, mode, *payload], cwd=d, + capture_output=True, text=True, timeout=60, env=os.environ) + return parse(out.stdout, out.stderr) + return run diff --git a/build/errors/bridges/ioredis_probe.mjs b/build/errors/bridges/ioredis_probe.mjs new file mode 100644 index 0000000000..9899d568a4 --- /dev/null +++ b/build/errors/bridges/ioredis_probe.mjs @@ -0,0 +1,35 @@ +// Runs one command, or one MULTI/EXEC block, through ioredis and prints what it returns or throws. +// Arguments: port user password mode payload... (see _common.py for the format) +import Redis from 'ioredis'; + +const [port, user, password, mode, ...payload] = process.argv.slice(2); +const arg = (s) => (s.startsWith('hex:') ? Buffer.from(s.slice(4), 'hex') : s); +const tokens = (p) => p.split('\x01').map(arg); +const one = (s) => String(s).replace(/[\r\n]+/g, ' '); +const describe = (e) => `EXC\t${e.constructor.name}\t${one(e.message)}`; + +// No reconnecting, so a refused or closed connection ends the probe. +const client = new Redis({ + host: '127.0.0.1', port: Number(port), connectTimeout: 5000, retryStrategy: () => null, + // By default ioredis waits while the server is loading its data. IOREDIS_READY_CHECK=0 turns that off. + ...(process.env.IOREDIS_READY_CHECK === '0' ? { enableReadyCheck: false } : {}), + ...(user ? { username: user } : {}), ...(password ? { password } : {}), +}); +let connectError = null; +client.on('error', (e) => { connectError = e; }); + +try { + if (mode === 'cmd') { + const [name, ...args] = tokens(payload[0]); + console.log('OK\t' + one(await client.call(name, ...args))); + } else { + const m = client.multi(); + for (const p of payload) { const [name, ...args] = tokens(p); m.call(name, ...args); } + const r = await m.exec(); + console.log('OK\t' + one(JSON.stringify(r.map(([e, v]) => (e ? `${e.constructor.name}: ${e.message}` : v))))); + } +} catch (e) { + console.log(describe(e) + (connectError && connectError !== e ? ` [error event ${connectError.constructor.name}: ${one(connectError.message)}]` : '')); +} finally { + client.disconnect(); +} diff --git a/build/errors/bridges/predis.py b/build/errors/bridges/predis.py new file mode 100644 index 0000000000..53d3bbe02f --- /dev/null +++ b/build/errors/bridges/predis.py @@ -0,0 +1,22 @@ +"""Predis bridge for live_clients.py. Needs PHP 8.1+ and Composer, and network access to Packagist.""" +import os, subprocess +from pathlib import Path +from _common import parse, sh + +KEY = 'predis' +VERSION = '3.5.1' +HERE = Path(__file__).parent + + +def prepare(version, cache): + d = Path(cache) / 'bridges' / f'predis-{version}' + d.mkdir(parents=True, exist_ok=True) + if not (d / 'vendor' / 'autoload.php').exists(): + sh('composer', 'require', '--no-interaction', '--no-progress', f'predis/predis:{version}', cwd=d) + (d / 'probe.php').write_text((HERE / 'predis_probe.php').read_text()) + + def run(port, user, password, mode, *payload): + out = subprocess.run(['php', 'probe.php', str(port), user, password, mode, *payload], cwd=d, + capture_output=True, text=True, timeout=60, env=os.environ) + return parse(out.stdout, out.stderr) + return run diff --git a/build/errors/bridges/predis_probe.php b/build/errors/bridges/predis_probe.php new file mode 100644 index 0000000000..3a667729a2 --- /dev/null +++ b/build/errors/bridges/predis_probe.php @@ -0,0 +1,30 @@ + str_starts_with($s, 'hex:') ? hex2bin(substr($s, 4)) : $s, explode("\x01", $p)); } +function raw($p) { $t = toks($p); return new Predis\Command\RawCommand(array_shift($t), $t); } +function one($s) { return str_replace(["\r", "\n"], ' ', (string) $s); } + +$params = ['scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => (int) $port, 'timeout' => 5.0, 'read_write_timeout' => 5.0]; +if ($user !== '') $params['username'] = $user; +if ($pass !== '') $params['password'] = $pass; +try { + $c = new Predis\Client($params); + if ($mode === 'cmd') { + $r = $c->executeCommand(raw($payload[0])); + echo "OK\t" . one(json_encode($r)) . "\n"; + } else { + $r = $c->transaction(function ($tx) use ($payload) { + foreach ($payload as $p) $tx->executeCommand(raw($p)); + }); + echo "OK\t" . one(json_encode($r)) . "\n"; + } +} catch (Throwable $e) { + $prev = $e->getPrevious(); + $type = $e instanceof Predis\Response\ServerException ? ' [getErrorType ' . $e->getErrorType() . ']' : ''; + echo "EXC\t" . get_class($e) . "\t" . one($e->getMessage()) . $type . ($prev ? ' [previous ' . get_class($prev) . ': ' . one($prev->getMessage()) . ']' : '') . "\n"; +} diff --git a/build/errors/bridges/redis_rb.py b/build/errors/bridges/redis_rb.py new file mode 100644 index 0000000000..a5101530eb --- /dev/null +++ b/build/errors/bridges/redis_rb.py @@ -0,0 +1,23 @@ +"""redis-rb bridge for live_clients.py. Needs Ruby 3.1+ and RubyGems, and network access to rubygems.org.""" +import os, subprocess +from pathlib import Path +from _common import parse, sh + +KEY = 'redis_rb' +VERSION = '6.0.0' +HERE = Path(__file__).parent + + +def prepare(version, cache): + d = Path(cache) / 'bridges' / f'redis-rb-{version}' + d.mkdir(parents=True, exist_ok=True) + env = {'GEM_HOME': str(d / 'gems'), 'GEM_PATH': str(d / 'gems')} + if not (d / 'gems').exists(): + sh('gem', 'install', 'redis', '-v', version, '--no-document', '--install-dir', str(d / 'gems'), env=env) + (d / 'probe.rb').write_text((HERE / 'redis_rb_probe.rb').read_text()) + + def run(port, user, password, mode, *payload): + out = subprocess.run(['ruby', 'probe.rb', str(port), user, password, mode, *payload], cwd=d, + capture_output=True, text=True, timeout=60, env={**os.environ, **env}) + return parse(out.stdout, out.stderr) + return run diff --git a/build/errors/bridges/redis_rb_probe.rb b/build/errors/bridges/redis_rb_probe.rb new file mode 100644 index 0000000000..43f22a5145 --- /dev/null +++ b/build/errors/bridges/redis_rb_probe.rb @@ -0,0 +1,23 @@ +# Runs one command, or one MULTI/EXEC block, through redis-rb and prints what it returns or raises. +# Arguments: port user password mode payload... (see _common.py for the format) +require 'redis' + +port, user, pass, mode, *payload = ARGV +def toks(p) = p.split("\x01", -1).map { |s| s.start_with?('hex:') ? [s[4..]].pack('H*') : s } +def one(s) = s.to_s.gsub(/[\r\n]+/, ' ') + +begin + opts = { host: '127.0.0.1', port: port.to_i, timeout: 5.0 } + opts[:username] = user unless user.empty? + opts[:password] = pass unless pass.empty? + r = Redis.new(**opts) + if mode == 'cmd' + puts "OK\t#{one(r.call(*toks(payload[0])).inspect)}" + else + res = r.multi { |tx| payload.each { |p| tx.call(*toks(p)) } } + puts "OK\t#{one(res.inspect)}" + end +rescue Exception => e + cause = e.cause ? " [cause #{e.cause.class}: #{one(e.cause.message)}]" : '' + puts "EXC\t#{e.class}\t#{one(e.message)}#{cause}" +end diff --git a/build/errors/bridges/rust_async.py b/build/errors/bridges/rust_async.py new file mode 100644 index 0000000000..bc4180f6c3 --- /dev/null +++ b/build/errors/bridges/rust_async.py @@ -0,0 +1,8 @@ +"""redis-rs (async API) bridge for live_clients.py.""" +from rust_redis import VERSION, prepare_api + +KEY = 'redis_rs_async' + + +def prepare(version, cache): + return prepare_api('async', version, cache) diff --git a/build/errors/bridges/rust_redis.py b/build/errors/bridges/rust_redis.py new file mode 100644 index 0000000000..311a76a140 --- /dev/null +++ b/build/errors/bridges/rust_redis.py @@ -0,0 +1,32 @@ +"""redis-rs bridges for live_clients.py. Needs Rust 1.88+ and Cargo, and network access to crates.io. +rust_sync.py and rust_async.py share this code.""" +import os, subprocess +from pathlib import Path +from _common import parse, sh + +HERE = Path(__file__).parent +VERSION = '1.6.0' + +CARGO = '''[package] +name = "probe" +version = "0.1.0" +edition = "2021" + +[dependencies] +redis = {{ version = "={v}", features = ["tokio-comp"] }} +tokio = {{ version = "1", features = ["rt-multi-thread", "macros"] }} +''' + + +def prepare_api(api, version, cache): + d = Path(cache) / 'bridges' / f'redis-rs-{version}' + (d / 'src').mkdir(parents=True, exist_ok=True) + (d / 'Cargo.toml').write_text(CARGO.format(v=version)) + (d / 'src' / 'main.rs').write_text((HERE / 'rust_redis_src' / 'main.rs').read_text()) + sh('cargo', 'build', '--release', '--quiet', cwd=d) + + def run(port, user, password, mode, *payload): + out = subprocess.run([str(d / 'target' / 'release' / 'probe'), api, str(port), user, password, mode, *payload], + capture_output=True, text=True, timeout=60, env=os.environ) + return parse(out.stdout, out.stderr) + return run diff --git a/build/errors/bridges/rust_redis_src/main.rs b/build/errors/bridges/rust_redis_src/main.rs new file mode 100644 index 0000000000..34398463ef --- /dev/null +++ b/build/errors/bridges/rust_redis_src/main.rs @@ -0,0 +1,61 @@ +// Runs one command, or one MULTI/EXEC block, through redis-rs and prints what it returns or the error it gives. +// Arguments: api port user password mode payload... (see _common.py for the format) +use redis::Value; +use std::time::Duration; + +fn one(s: &str) -> String { s.replace(['\r', '\n'], " ") } + +fn cmd_of(p: &str) -> redis::Cmd { + let mut parts = p.split('\x01'); + let mut c = redis::cmd(parts.next().unwrap()); + for s in parts { + if let Some(h) = s.strip_prefix("hex:") { + let b: Vec = (0..h.len() / 2).map(|i| u8::from_str_radix(&h[2 * i..2 * i + 2], 16).unwrap()).collect(); + c.arg(&b[..]); + } else { + c.arg(s); + } + } + c +} + +fn describe(e: &redis::RedisError) -> String { + format!("EXC\tErrorKind::{:?}\t{} [code {:?}, detail {:?}]", e.kind(), one(&e.to_string()), e.code(), e.detail().map(one)) +} + +fn sync_run(url: &str, mode: &str, payload: &[String]) -> Result { + let client = redis::Client::open(url)?; + let mut con = client.get_connection_with_timeout(Duration::from_secs(5))?; + if mode == "cmd" { cmd_of(&payload[0]).query::(&mut con) } else { + let mut p = redis::pipe(); p.atomic(); + for s in payload { p.add_command(cmd_of(s)); } + p.query::(&mut con) + } +} + +async fn async_run(url: &str, mode: &str, payload: &[String]) -> Result { + let client = redis::Client::open(url)?; + let mut con = client.get_multiplexed_async_connection().await?; + if mode == "cmd" { cmd_of(&payload[0]).query_async::(&mut con).await } else { + let mut p = redis::pipe(); p.atomic(); + for s in payload { p.add_command(cmd_of(s)); } + p.query_async::(&mut con).await + } +} + +#[tokio::main] +async fn main() { + let a: Vec = std::env::args().skip(1).collect(); + let (api, port, user, pass, mode) = (&a[0], &a[1], &a[2], &a[3], &a[4]); + let auth = if !user.is_empty() { format!("{user}:{pass}@") } else if !pass.is_empty() { format!(":{pass}@") } else { String::new() }; + let url = format!("redis://{auth}127.0.0.1:{port}/"); + let payload = &a[5..]; + let r = if api == "sync" { + let (u, m, p) = (url.clone(), mode.clone(), payload.to_vec()); + tokio::task::spawn_blocking(move || sync_run(&u, &m, &p)).await.unwrap() + } else { async_run(&url, mode, payload).await }; + match r { + Ok(v) => println!("OK\t{}", one(&format!("{v:?}"))), + Err(e) => println!("{}", describe(&e)), + } +} diff --git a/build/errors/bridges/rust_sync.py b/build/errors/bridges/rust_sync.py new file mode 100644 index 0000000000..c5cc39aa56 --- /dev/null +++ b/build/errors/bridges/rust_sync.py @@ -0,0 +1,8 @@ +"""redis-rs (sync API) bridge for live_clients.py.""" +from rust_redis import VERSION, prepare_api + +KEY = 'redis_rs_sync' + + +def prepare(version, cache): + return prepare_api('sync', version, cache) diff --git a/build/errors/bridges/se_redis.py b/build/errors/bridges/se_redis.py new file mode 100644 index 0000000000..862e9c42e8 --- /dev/null +++ b/build/errors/bridges/se_redis.py @@ -0,0 +1,28 @@ +"""StackExchange.Redis bridges for live_clients.py (NRedisStack builds on this library). +Needs the .NET 9 SDK, and network access to NuGet. se_redis_sync.py and se_redis_async.py share this code.""" +import os, subprocess +from pathlib import Path +from _common import parse, sh + +HERE = Path(__file__).parent +VERSION = '3.0.0' + +CSPROJ = '''Exenet9.0 +enabledisableprobe + +''' + + +def prepare_api(api, version, cache): + d = Path(cache) / 'bridges' / f'se-redis-{version}' + d.mkdir(parents=True, exist_ok=True) + (d / 'probe.csproj').write_text(CSPROJ.format(v=version)) + (d / 'Program.cs').write_text((HERE / 'SeRedisProbe.cs').read_text()) + env = {'DOTNET_CLI_TELEMETRY_OPTOUT': '1', 'DOTNET_NOLOGO': '1'} + sh('dotnet', 'build', '-c', 'Release', '-o', 'out', '--nologo', '-v', 'q', cwd=d, env=env) + + def run(port, user, password, mode, *payload): + out = subprocess.run(['dotnet', 'out/probe.dll', api, str(port), user, password, mode, *payload], cwd=d, + capture_output=True, text=True, timeout=60, env={**os.environ, **env}) + return parse(out.stdout, out.stderr) + return run diff --git a/build/errors/bridges/se_redis_async.py b/build/errors/bridges/se_redis_async.py new file mode 100644 index 0000000000..fc6c08fa81 --- /dev/null +++ b/build/errors/bridges/se_redis_async.py @@ -0,0 +1,8 @@ +"""StackExchange.Redis (async API) bridge for live_clients.py.""" +from se_redis import VERSION, prepare_api + +KEY = 'se_redis_async' + + +def prepare(version, cache): + return prepare_api('async', version, cache) diff --git a/build/errors/bridges/se_redis_sync.py b/build/errors/bridges/se_redis_sync.py new file mode 100644 index 0000000000..f42dc40fea --- /dev/null +++ b/build/errors/bridges/se_redis_sync.py @@ -0,0 +1,8 @@ +"""StackExchange.Redis (sync API) bridge for live_clients.py.""" +from se_redis import VERSION, prepare_api + +KEY = 'se_redis_sync' + + +def prepare(version, cache): + return prepare_api('sync', version, cache) diff --git a/build/errors/client_baselines/ioredis.json b/build/errors/client_baselines/ioredis.json new file mode 100644 index 0000000000..808df61bef --- /dev/null +++ b/build/errors/client_baselines/ioredis.json @@ -0,0 +1,368 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "ioredis": "5.11.1" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "ioredis": [ + "ReplyError: ERR unknown command 'FOO', with args beginning with: 'bar' " + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "ioredis": [ + "ReplyError: ERR wrong number of arguments for 'get' command" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "ioredis": [ + "ReplyError: ERR syntax error" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "ioredis": [ + "ReplyError: ERR value is not an integer or out of range" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "ioredis": [ + "ReplyError: WRONGTYPE Operation against a key holding the wrong kind of value" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "ioredis": [ + "ReplyError: EXECABORT Transaction discarded because of previous errors." + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "ioredis": [ + "ReplyError: NOSCRIPT No matching script. Please use EVAL." + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "ioredis": [ + "ReplyError: BUSYKEY Target key name already exists." + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "ioredis": [ + "ReplyError: BUSYGROUP Consumer Group name already exists" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "ioredis": [ + "ReplyError: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "ioredis": [ + "ReplyError: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "ReplyError: WRONGPASS invalid username-password pair or user is disabled." + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "ioredis": [ + "ReplyError: OOM command not allowed when used memory > 'maxmemory'." + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "ioredis": [ + "ReplyError: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE." + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "ioredis": [ + "Error: Connection is closed." + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "ioredis": [ + "ReplyError: NOREPLICAS Not enough good replicas to write." + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "ioredis": [ + "ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-106.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "ioredis": [ + "ReplyError: NOAUTH Authentication required." + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "ioredis": [ + "ReplyError: NOPERM User app has no permissions to run the 'set' command", + "ReplyError: NOPERM No permissions to access a key" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "info", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "ioredis": [ + "Error: Connection is closed." + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "ioredis": [ + "ReplyError: READONLY You can't write against a read only replica." + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "ioredis": [ + "ReplyError: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "ioredis": [ + "ReplyError: MOVED 12182 127.0.0.1:6573" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "ioredis": [ + "ReplyError: CROSSSLOT Keys in request don't hash to the same slot" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "ioredis": [ + "ReplyError: ASK 12182 127.0.0.1:6571" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "ioredis": [ + "ReplyError: TRYAGAIN Multiple keys request during rehashing of slot" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "ioredis": [ + "ReplyError: CLUSTERDOWN Hash slot not served", + "ReplyError: CLUSTERDOWN The cluster is down" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "ioredis": [ + "ReplyError: LOADING Redis is loading the dataset in memory" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793718814", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2696000", + "loading_loaded_bytes:11263", + "loading_loaded_perc:1.69", + "loading_eta_seconds:58" + ], + "ioredis run with IOREDIS_READY_CHECK=0 (enableReadyCheck: false); by default it waits while the server loads" + ] + } +} diff --git a/build/errors/client_baselines/predis.json b/build/errors/client_baselines/predis.json new file mode 100644 index 0000000000..0043de1333 --- /dev/null +++ b/build/errors/client_baselines/predis.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "predis": "3.5.1" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "predis": [ + "Predis\\Response\\ServerException: ERR unknown command 'FOO', with args beginning with: 'bar' [getErrorType ERR]" + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "predis": [ + "Predis\\Response\\ServerException: ERR wrong number of arguments for 'get' command [getErrorType ERR]" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "predis": [ + "Predis\\Response\\ServerException: ERR syntax error [getErrorType ERR]" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "predis": [ + "Predis\\Response\\ServerException: ERR value is not an integer or out of range [getErrorType ERR]" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "predis": [ + "Predis\\Response\\ServerException: WRONGTYPE Operation against a key holding the wrong kind of value [getErrorType WRONGTYPE]" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "predis": [ + "Predis\\Transaction\\AbortedMultiExecException: ERR unknown command 'FOO'" + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "predis": [ + "Predis\\Response\\ServerException: NOSCRIPT No matching script. Please use EVAL. [getErrorType NOSCRIPT]" + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "predis": [ + "Predis\\Response\\ServerException: BUSYKEY Target key name already exists. [getErrorType BUSYKEY]" + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "predis": [ + "Predis\\Response\\ServerException: BUSYGROUP Consumer Group name already exists [getErrorType BUSYGROUP]" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "predis": [ + "Predis\\Response\\ServerException: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option [getErrorType NOGROUP]" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "predis": [ + "Predis\\Response\\ServerException: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct? [getErrorType ERR]", + "Predis\\Connection\\ConnectionException: Failed: WRONGPASS invalid username-password pair or user is disabled. [tcp://127.0.0.1:6560]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "predis": [ + "Predis\\Response\\ServerException: OOM command not allowed when used memory > 'maxmemory'. [getErrorType OOM]" + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "predis": [ + "Predis\\Response\\ServerException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. [getErrorType BUSY]" + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "predis": [ + "Predis\\Connection\\ConnectionException: Stream is already at the end [tcp://127.0.0.1:6557]" + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "predis": [ + "Predis\\Response\\ServerException: NOREPLICAS Not enough good replicas to write. [getErrorType NOREPLICAS]" + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "predis": [ + "Predis\\Response\\ServerException: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. [getErrorType MISCONF]" + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-107.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "predis": [ + "Predis\\Response\\ServerException: NOAUTH Authentication required. [getErrorType NOAUTH]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "predis": [ + "Predis\\Response\\ServerException: NOPERM User app has no permissions to run the 'set' command [getErrorType NOPERM]", + "Predis\\Response\\ServerException: NOPERM No permissions to access a key [getErrorType NOPERM]" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "predis": [ + "Predis\\Connection\\ConnectionException: Stream is already at the end [tcp://127.0.0.1:6565]" + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "predis": [ + "Predis\\Response\\ServerException: READONLY You can't write against a read only replica. [getErrorType READONLY]" + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "predis": [ + "Predis\\Response\\ServerException: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. [getErrorType MASTERDOWN]" + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "predis": [ + "Predis\\Response\\ServerException: LOADING Redis is loading the dataset in memory [getErrorType LOADING]" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793718745", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2696208", + "loading_loaded_bytes:7159", + "loading_loaded_perc:1.07", + "loading_eta_seconds:92" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "predis": [ + "Predis\\Response\\ServerException: MOVED 12182 127.0.0.1:6573 [getErrorType MOVED]" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "predis": [ + "Predis\\Response\\ServerException: CROSSSLOT Keys in request don't hash to the same slot [getErrorType CROSSSLOT]" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "predis": [ + "Predis\\Response\\ServerException: ASK 12182 127.0.0.1:6571 [getErrorType ASK]" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "predis": [ + "Predis\\Response\\ServerException: TRYAGAIN Multiple keys request during rehashing of slot [getErrorType TRYAGAIN]" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "predis": [ + "Predis\\Response\\ServerException: CLUSTERDOWN Hash slot not served [getErrorType CLUSTERDOWN]", + "Predis\\Response\\ServerException: CLUSTERDOWN The cluster is down [getErrorType CLUSTERDOWN]" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/build/errors/client_baselines/redis_rb.json b/build/errors/client_baselines/redis_rb.json new file mode 100644 index 0000000000..9bbf11b0b1 --- /dev/null +++ b/build/errors/client_baselines/redis_rb.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "redis_rb": "6.0.0" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "redis_rb": [ + "Redis::CommandError: ERR unknown command 'FOO', with args beginning with: 'bar' (redis://127.0.0.1:6550) [cause RedisClient::CommandError: ERR unknown command 'FOO', with args beginning with: 'bar' (redis://127.0.0.1:6550)]" + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "redis_rb": [ + "Redis::CommandError: ERR wrong number of arguments for 'get' command (redis://127.0.0.1:6550) [cause RedisClient::CommandError: ERR wrong number of arguments for 'get' command (redis://127.0.0.1:6550)]" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "redis_rb": [ + "Redis::CommandError: ERR syntax error (redis://127.0.0.1:6550) [cause RedisClient::CommandError: ERR syntax error (redis://127.0.0.1:6550)]" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "redis_rb": [ + "Redis::CommandError: ERR value is not an integer or out of range (redis://127.0.0.1:6550) [cause RedisClient::CommandError: ERR value is not an integer or out of range (redis://127.0.0.1:6550)]" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "redis_rb": [ + "Redis::WrongTypeError: WRONGTYPE Operation against a key holding the wrong kind of value (redis://127.0.0.1:6550) [cause RedisClient::WrongTypeError: WRONGTYPE Operation against a key holding the wrong kind of value (redis://127.0.0.1:6550)]" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "redis_rb": [ + "Redis::CommandError: ERR unknown command 'FOO' (redis://127.0.0.1:6550) [cause RedisClient::CommandError: ERR unknown command 'FOO' (redis://127.0.0.1:6550)]" + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "redis_rb": [ + "Redis::NoScriptError: NOSCRIPT No matching script. Please use EVAL. (redis://127.0.0.1:6550) [cause RedisClient::NoScriptError: NOSCRIPT No matching script. Please use EVAL. (redis://127.0.0.1:6550)]" + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "redis_rb": [ + "Redis::CommandError: BUSYKEY Target key name already exists. (redis://127.0.0.1:6550) [cause RedisClient::CommandError: BUSYKEY Target key name already exists. (redis://127.0.0.1:6550)]" + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "redis_rb": [ + "Redis::CommandError: BUSYGROUP Consumer Group name already exists (redis://127.0.0.1:6550) [cause RedisClient::CommandError: BUSYGROUP Consumer Group name already exists (redis://127.0.0.1:6550)]" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "redis_rb": [ + "Redis::CommandError: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option (redis://127.0.0.1:6550) [cause RedisClient::CommandError: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option (redis://127.0.0.1:6550)]" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "redis_rb": [ + "Redis::CommandError: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct? (redis://127.0.0.1:6550) [cause RedisClient::CommandError: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct? (redis://127.0.0.1:6550)]", + "Redis::CannotConnectError: WRONGPASS invalid username-password pair or user is disabled. (redis://127.0.0.1:6560) [cause RedisClient::AuthenticationError: WRONGPASS invalid username-password pair or user is disabled. (redis://127.0.0.1:6560)]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "redis_rb": [ + "Redis::OutOfMemoryError: OOM command not allowed when used memory > 'maxmemory'. (redis://127.0.0.1:6550) [cause RedisClient::OutOfMemoryError: OOM command not allowed when used memory > 'maxmemory'. (redis://127.0.0.1:6550)]" + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "redis_rb": [ + "Redis::CommandError: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. (redis://127.0.0.1:6550) [cause RedisClient::CommandError: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. (redis://127.0.0.1:6550)]" + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "redis_rb": [ + "Redis::CannotConnectError: EOFError (redis://127.0.0.1:6557) (redis://127.0.0.1:6557) [cause RedisClient::CannotConnectError: EOFError (redis://127.0.0.1:6557) (redis://127.0.0.1:6557)]" + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "redis_rb": [ + "Redis::CommandError: NOREPLICAS Not enough good replicas to write. (redis://127.0.0.1:6558) [cause RedisClient::CommandError: NOREPLICAS Not enough good replicas to write. (redis://127.0.0.1:6558)]" + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "redis_rb": [ + "Redis::CommandError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. (redis://127.0.0.1:6559) [cause RedisClient::CommandError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. (redis://127.0.0.1:6559)]" + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-107.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "redis_rb": [ + "Redis::CommandError: NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time (redis://127.0.0.1:6560) [cause RedisClient::CommandError: NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time (redis://127.0.0.1:6560)]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "redis_rb": [ + "Redis::PermissionError: NOPERM User app has no permissions to run the 'set' command (redis://127.0.0.1:6560) [cause RedisClient::PermissionError: NOPERM User app has no permissions to run the 'set' command (redis://127.0.0.1:6560)]", + "Redis::PermissionError: NOPERM No permissions to access a key (redis://127.0.0.1:6560) [cause RedisClient::PermissionError: NOPERM No permissions to access a key (redis://127.0.0.1:6560)]" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "redis_rb": [ + "Redis::CannotConnectError: EOFError (redis://127.0.0.1:6565) (redis://127.0.0.1:6565) [cause RedisClient::CannotConnectError: EOFError (redis://127.0.0.1:6565) (redis://127.0.0.1:6565)]" + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "redis_rb": [ + "Redis::ReadOnlyError: READONLY You can't write against a read only replica. (redis://127.0.0.1:6561) [cause RedisClient::ReadOnlyError: READONLY You can't write against a read only replica. (redis://127.0.0.1:6561)]" + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "redis_rb": [ + "Redis::ConnectionError: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. (redis://127.0.0.1:6563) [cause RedisClient::MasterDownError: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. (redis://127.0.0.1:6563)]" + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "redis_rb": [ + "Redis::CommandError: LOADING Redis is loading the dataset in memory (redis://127.0.0.1:6564) [cause RedisClient::CommandError: LOADING Redis is loading the dataset in memory (redis://127.0.0.1:6564)]" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793718614", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2696080", + "loading_loaded_bytes:8181", + "loading_loaded_perc:1.22", + "loading_eta_seconds:1" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "redis_rb": [ + "Redis::CommandError: MOVED 12182 127.0.0.1:6573 (redis://127.0.0.1:6571) [cause RedisClient::CommandError: MOVED 12182 127.0.0.1:6573 (redis://127.0.0.1:6571)]" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "redis_rb": [ + "Redis::CommandError: CROSSSLOT Keys in request don't hash to the same slot (redis://127.0.0.1:6573) [cause RedisClient::CommandError: CROSSSLOT Keys in request don't hash to the same slot (redis://127.0.0.1:6573)]" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "redis_rb": [ + "Redis::CommandError: ASK 12182 127.0.0.1:6571 (redis://127.0.0.1:6573) [cause RedisClient::CommandError: ASK 12182 127.0.0.1:6571 (redis://127.0.0.1:6573)]" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "redis_rb": [ + "Redis::CommandError: TRYAGAIN Multiple keys request during rehashing of slot (redis://127.0.0.1:6573) [cause RedisClient::CommandError: TRYAGAIN Multiple keys request during rehashing of slot (redis://127.0.0.1:6573)]" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "redis_rb": [ + "Redis::CommandError: CLUSTERDOWN Hash slot not served (redis://127.0.0.1:6574) [cause RedisClient::CommandError: CLUSTERDOWN Hash slot not served (redis://127.0.0.1:6574)]", + "Redis::CommandError: CLUSTERDOWN The cluster is down (redis://127.0.0.1:6573) [cause RedisClient::CommandError: CLUSTERDOWN The cluster is down (redis://127.0.0.1:6573)]" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/build/errors/client_baselines/rust_async.json b/build/errors/client_baselines/rust_async.json new file mode 100644 index 0000000000..c6833dda0e --- /dev/null +++ b/build/errors/client_baselines/rust_async.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "redis_rs_async": "1.6.0" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "redis_rs_async": [ + "ErrorKind::Server(ResponseError): ResponseError: unknown command 'FOO', with args beginning with: 'bar' [code Some(\"ERR\"), detail Some(\"unknown command 'FOO', with args beginning with: 'bar' \")]" + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "redis_rs_async": [ + "ErrorKind::Server(ResponseError): ResponseError: wrong number of arguments for 'get' command [code Some(\"ERR\"), detail Some(\"wrong number of arguments for 'get' command\")]" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "redis_rs_async": [ + "ErrorKind::Server(ResponseError): ResponseError: syntax error [code Some(\"ERR\"), detail Some(\"syntax error\")]" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "redis_rs_async": [ + "ErrorKind::Server(ResponseError): ResponseError: value is not an integer or out of range [code Some(\"ERR\"), detail Some(\"value is not an integer or out of range\")]" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "redis_rs_async": [ + "ErrorKind::Extension: \"WRONGTYPE\": Operation against a key holding the wrong kind of value [code Some(\"WRONGTYPE\"), detail Some(\"Operation against a key holding the wrong kind of value\")]" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "redis_rs_async": [ + "ErrorKind::Server(ExecAbort): Transaction aborted: [(Index 0, error: ResponseError: unknown command 'FOO')] [code Some(\"EXECABORT\"), detail None]" + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "redis_rs_async": [ + "ErrorKind::Server(NoScript): NoScript: No matching script. Please use EVAL. [code Some(\"NOSCRIPT\"), detail Some(\"No matching script. Please use EVAL.\")]" + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "redis_rs_async": [ + "ErrorKind::Extension: \"BUSYKEY\": Target key name already exists. [code Some(\"BUSYKEY\"), detail Some(\"Target key name already exists.\")]" + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "redis_rs_async": [ + "ErrorKind::Extension: \"BUSYGROUP\": Consumer Group name already exists [code Some(\"BUSYGROUP\"), detail Some(\"Consumer Group name already exists\")]" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "redis_rs_async": [ + "ErrorKind::Extension: \"NOGROUP\": No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option [code Some(\"NOGROUP\"), detail Some(\"No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option\")]" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "redis_rs_async": [ + "ErrorKind::Server(ResponseError): ResponseError: AUTH called without any password configured for the default user. Are you sure your configuration is correct? [code Some(\"ERR\"), detail Some(\"AUTH called without any password configured for the default user. Are you sure your configuration is correct?\")]", + "ErrorKind::AuthenticationFailed: Password authentication failed - AuthenticationFailed [code None, detail None]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "redis_rs_async": [ + "ErrorKind::Extension: \"OOM\": command not allowed when used memory > 'maxmemory'. [code Some(\"OOM\"), detail Some(\"command not allowed when used memory > 'maxmemory'.\")]" + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "redis_rs_async": [ + "ErrorKind::Extension: \"BUSY\": Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. [code Some(\"BUSY\"), detail Some(\"Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.\")]" + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "redis_rs_async": [ + "ErrorKind::Io: broken pipe [code None, detail None]" + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "redis_rs_async": [ + "ErrorKind::Extension: \"NOREPLICAS\": Not enough good replicas to write. [code Some(\"NOREPLICAS\"), detail Some(\"Not enough good replicas to write.\")]" + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "redis_rs_async": [ + "ErrorKind::Extension: \"MISCONF\": Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. [code Some(\"MISCONF\"), detail Some(\"Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\")]" + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-108.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "redis_rs_async": [ + "ErrorKind::Extension: \"NOAUTH\": Authentication required. [code Some(\"NOAUTH\"), detail Some(\"Authentication required.\")]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "redis_rs_async": [ + "ErrorKind::Server(NoPerm): NoPerm: User app has no permissions to run the 'set' command [code Some(\"NOPERM\"), detail Some(\"User app has no permissions to run the 'set' command\")]", + "ErrorKind::Server(NoPerm): NoPerm: No permissions to access a key [code Some(\"NOPERM\"), detail Some(\"No permissions to access a key\")]" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "redis_rs_async": [ + "ErrorKind::Io: broken pipe [code None, detail None]" + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "redis_rs_async": [ + "ErrorKind::Server(ReadOnly): ReadOnly: You can't write against a read only replica. [code Some(\"READONLY\"), detail Some(\"You can't write against a read only replica.\")]" + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "redis_rs_async": [ + "ErrorKind::Server(MasterDown): MasterDown: Link with MASTER is down and replica-serve-stale-data is set to 'no'. [code Some(\"MASTERDOWN\"), detail Some(\"Link with MASTER is down and replica-serve-stale-data is set to 'no'.\")]" + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "redis_rs_async": [ + "ErrorKind::Server(BusyLoading): BusyLoading: Redis is loading the dataset in memory [code Some(\"LOADING\"), detail Some(\"Redis is loading the dataset in memory\")]" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793718657", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2696464", + "loading_loaded_bytes:5119", + "loading_loaded_perc:0.77", + "loading_eta_seconds:1" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "redis_rs_async": [ + "ErrorKind::Server(Moved): Moved: 12182 127.0.0.1:6573 [code Some(\"MOVED\"), detail Some(\"12182 127.0.0.1:6573\")]" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "redis_rs_async": [ + "ErrorKind::Server(CrossSlot): CrossSlot: Keys in request don't hash to the same slot [code Some(\"CROSSSLOT\"), detail Some(\"Keys in request don't hash to the same slot\")]" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "redis_rs_async": [ + "ErrorKind::Server(Ask): Ask: 12182 127.0.0.1:6571 [code Some(\"ASK\"), detail Some(\"12182 127.0.0.1:6571\")]" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "redis_rs_async": [ + "ErrorKind::Server(TryAgain): TryAgain: Multiple keys request during rehashing of slot [code Some(\"TRYAGAIN\"), detail Some(\"Multiple keys request during rehashing of slot\")]" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "redis_rs_async": [ + "ErrorKind::Server(ClusterDown): ClusterDown: Hash slot not served [code Some(\"CLUSTERDOWN\"), detail Some(\"Hash slot not served\")]", + "ErrorKind::Server(ClusterDown): ClusterDown: The cluster is down [code Some(\"CLUSTERDOWN\"), detail Some(\"The cluster is down\")]" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/build/errors/client_baselines/rust_sync.json b/build/errors/client_baselines/rust_sync.json new file mode 100644 index 0000000000..7360f58c3d --- /dev/null +++ b/build/errors/client_baselines/rust_sync.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "redis_rs_sync": "1.6.0" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "redis_rs_sync": [ + "ErrorKind::Server(ResponseError): ResponseError: unknown command 'FOO', with args beginning with: 'bar' [code Some(\"ERR\"), detail Some(\"unknown command 'FOO', with args beginning with: 'bar' \")]" + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "redis_rs_sync": [ + "ErrorKind::Server(ResponseError): ResponseError: wrong number of arguments for 'get' command [code Some(\"ERR\"), detail Some(\"wrong number of arguments for 'get' command\")]" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "redis_rs_sync": [ + "ErrorKind::Server(ResponseError): ResponseError: syntax error [code Some(\"ERR\"), detail Some(\"syntax error\")]" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "redis_rs_sync": [ + "ErrorKind::Server(ResponseError): ResponseError: value is not an integer or out of range [code Some(\"ERR\"), detail Some(\"value is not an integer or out of range\")]" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "redis_rs_sync": [ + "ErrorKind::Extension: \"WRONGTYPE\": Operation against a key holding the wrong kind of value [code Some(\"WRONGTYPE\"), detail Some(\"Operation against a key holding the wrong kind of value\")]" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "redis_rs_sync": [ + "ErrorKind::Server(ExecAbort): Transaction aborted: [(Index 0, error: ResponseError: unknown command 'FOO')] [code Some(\"EXECABORT\"), detail None]" + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "redis_rs_sync": [ + "ErrorKind::Server(NoScript): NoScript: No matching script. Please use EVAL. [code Some(\"NOSCRIPT\"), detail Some(\"No matching script. Please use EVAL.\")]" + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "redis_rs_sync": [ + "ErrorKind::Extension: \"BUSYKEY\": Target key name already exists. [code Some(\"BUSYKEY\"), detail Some(\"Target key name already exists.\")]" + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "redis_rs_sync": [ + "ErrorKind::Extension: \"BUSYGROUP\": Consumer Group name already exists [code Some(\"BUSYGROUP\"), detail Some(\"Consumer Group name already exists\")]" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "redis_rs_sync": [ + "ErrorKind::Extension: \"NOGROUP\": No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option [code Some(\"NOGROUP\"), detail Some(\"No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option\")]" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "redis_rs_sync": [ + "ErrorKind::Server(ResponseError): ResponseError: AUTH called without any password configured for the default user. Are you sure your configuration is correct? [code Some(\"ERR\"), detail Some(\"AUTH called without any password configured for the default user. Are you sure your configuration is correct?\")]", + "ErrorKind::AuthenticationFailed: Password authentication failed - AuthenticationFailed [code None, detail None]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "redis_rs_sync": [ + "ErrorKind::Extension: \"OOM\": command not allowed when used memory > 'maxmemory'. [code Some(\"OOM\"), detail Some(\"command not allowed when used memory > 'maxmemory'.\")]" + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "redis_rs_sync": [ + "ErrorKind::Extension: \"BUSY\": Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. [code Some(\"BUSY\"), detail Some(\"Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.\")]" + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "redis_rs_sync": [ + "ErrorKind::Io: unexpected end of file [code None, detail None]" + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "redis_rs_sync": [ + "ErrorKind::Extension: \"NOREPLICAS\": Not enough good replicas to write. [code Some(\"NOREPLICAS\"), detail Some(\"Not enough good replicas to write.\")]" + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "redis_rs_sync": [ + "ErrorKind::Extension: \"MISCONF\": Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. [code Some(\"MISCONF\"), detail Some(\"Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\")]" + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-107.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "redis_rs_sync": [ + "ErrorKind::Extension: \"NOAUTH\": Authentication required. [code Some(\"NOAUTH\"), detail Some(\"Authentication required.\")]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "redis_rs_sync": [ + "ErrorKind::Server(NoPerm): NoPerm: User app has no permissions to run the 'set' command [code Some(\"NOPERM\"), detail Some(\"User app has no permissions to run the 'set' command\")]", + "ErrorKind::Server(NoPerm): NoPerm: No permissions to access a key [code Some(\"NOPERM\"), detail Some(\"No permissions to access a key\")]" + ], + "notes": [ + [ + { + "reason": "key", + "object": "other", + "username": "app" + }, + { + "reason": "command", + "object": "client|setinfo", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "redis_rs_sync": [ + "ErrorKind::Io: unexpected end of file [code None, detail None]" + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "redis_rs_sync": [ + "ErrorKind::Server(ReadOnly): ReadOnly: You can't write against a read only replica. [code Some(\"READONLY\"), detail Some(\"You can't write against a read only replica.\")]" + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "redis_rs_sync": [ + "ErrorKind::Server(MasterDown): MasterDown: Link with MASTER is down and replica-serve-stale-data is set to 'no'. [code Some(\"MASTERDOWN\"), detail Some(\"Link with MASTER is down and replica-serve-stale-data is set to 'no'.\")]" + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "redis_rs_sync": [ + "ErrorKind::Server(BusyLoading): BusyLoading: Redis is loading the dataset in memory [code Some(\"LOADING\"), detail Some(\"Redis is loading the dataset in memory\")]" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793718636", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2697104", + "loading_loaded_bytes:5115", + "loading_loaded_perc:0.77", + "loading_eta_seconds:1" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "redis_rs_sync": [ + "ErrorKind::Server(Moved): Moved: 12182 127.0.0.1:6573 [code Some(\"MOVED\"), detail Some(\"12182 127.0.0.1:6573\")]" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "redis_rs_sync": [ + "ErrorKind::Server(CrossSlot): CrossSlot: Keys in request don't hash to the same slot [code Some(\"CROSSSLOT\"), detail Some(\"Keys in request don't hash to the same slot\")]" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "redis_rs_sync": [ + "ErrorKind::Server(Ask): Ask: 12182 127.0.0.1:6571 [code Some(\"ASK\"), detail Some(\"12182 127.0.0.1:6571\")]" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "redis_rs_sync": [ + "ErrorKind::Server(TryAgain): TryAgain: Multiple keys request during rehashing of slot [code Some(\"TRYAGAIN\"), detail Some(\"Multiple keys request during rehashing of slot\")]" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "redis_rs_sync": [ + "ErrorKind::Server(ClusterDown): ClusterDown: Hash slot not served [code Some(\"CLUSTERDOWN\"), detail Some(\"Hash slot not served\")]", + "ErrorKind::Server(ClusterDown): ClusterDown: The cluster is down [code Some(\"CLUSTERDOWN\"), detail Some(\"The cluster is down\")]" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/build/errors/client_baselines/se_redis_async.json b/build/errors/client_baselines/se_redis_async.json new file mode 100644 index 0000000000..683d34ddcf --- /dev/null +++ b/build/errors/client_baselines/se_redis_async.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "se_redis_async": "3.0.0" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: ERR unknown command 'FOO', with args beginning with: 'bar' " + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: ERR wrong number of arguments for 'get' command" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: ERR syntax error" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: ERR value is not an integer or out of range" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: WRONGTYPE Operation against a key holding the wrong kind of value" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: EXECABORT Transaction discarded because of previous errors." + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: NOSCRIPT No matching script. Please use EVAL." + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: BUSYKEY Target key name already exists." + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: BUSYGROUP Consumer Group name already exists" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) SimpleError: WRONGPASS invalid username-password pair or user is disabled. [inner StackExchange.Redis.RedisServerException: SimpleError: WRONGPASS invalid username-password pair or user is disabled.]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: OOM command not allowed when used memory > 'maxmemory'." + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code." + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code." + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: NOREPLICAS Not enough good replicas to write." + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:0", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-107.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) NOAUTH Returned - connection has not yet authenticated [inner StackExchange.Redis.RedisServerException: NOAUTH Returned - connection has not yet authenticated]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code.", + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code." + ], + "notes": [ + [ + { + "reason": "command", + "object": "echo", + "username": "app" + }, + { + "reason": "key", + "object": "__Booksleeve_TieBreak", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code." + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisConnectionException: No connection (requires writable - not eligible for replica) is active/available to service this operation: SET, mc: 1/1/0, clientName: VFJ1GQNV29-Andrew-Gareth-Stark(SE.Redis-v3.0.0.11234), IOCP: (Busy=0,Free=1000,Min=1,Max=1000), WORKER: (Busy=1,Free=32766,Min=8,Max=32767), POOL: (Threads=3,QueuedItems=0,CompletedItems=10,Timers=1), v: 3.0.0.11234" + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: LOADING Redis is loading the dataset in memory" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793718572", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2695552", + "loading_loaded_bytes:8183", + "loading_loaded_perc:1.23", + "loading_eta_seconds:80" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "se_redis_async": [ + "no error" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: CROSSSLOT Keys in request don't hash to the same slot" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "se_redis_async": [ + "no error" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: TRYAGAIN Multiple keys request during rehashing of slot" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "se_redis_async": [ + "StackExchange.Redis.RedisServerException: CLUSTERDOWN Hash slot not served", + "StackExchange.Redis.RedisServerException: CLUSTERDOWN The cluster is down" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/build/errors/client_baselines/se_redis_sync.json b/build/errors/client_baselines/se_redis_sync.json new file mode 100644 index 0000000000..bc0e674525 --- /dev/null +++ b/build/errors/client_baselines/se_redis_sync.json @@ -0,0 +1,367 @@ +{ + "_meta": { + "redis": "Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=26142e2a33b527c", + "redis_py": null, + "date": "2026-11-03", + "se_redis_sync": "3.0.0" + }, + "err-unknown-command": { + "raw": [ + "-ERR unknown command 'FOO', with args beginning with: 'bar' ", + "-ERR unknown command 'FOO'", + "-ERR unknown subcommand 'FOO'. Try CONFIG HELP." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: ERR unknown command 'FOO', with args beginning with: 'bar' " + ] + }, + "err-wrong-number-of-arguments": { + "raw": [ + "-ERR wrong number of arguments for 'get' command", + "-ERR wrong number of arguments for 'config|get' command" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: ERR wrong number of arguments for 'get' command" + ] + }, + "err-syntax-error": { + "raw": [ + "-ERR syntax error" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: ERR syntax error" + ] + }, + "err-not-an-integer": { + "raw": [ + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR value is not an integer or out of range", + "-ERR increment or decrement would overflow", + "-ERR value is not a valid float" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: ERR value is not an integer or out of range" + ], + "notes": [ + "INCR on 1.5", + "INCR on int64 max", + "INCRBYFLOAT on abc" + ] + }, + "wrongtype": { + "raw": [ + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "-WRONGTYPE Operation against a key holding the wrong kind of value", + "+list" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: WRONGTYPE Operation against a key holding the wrong kind of value" + ], + "notes": [ + "TYPE" + ] + }, + "execabort": { + "raw": [ + "-ERR unknown command 'FOO'", + "-EXECABORT Transaction discarded because of previous errors.", + [ + "-ERR value is not an integer or out of range" + ], + "-ERR wrong number of arguments for 'get' command", + "-EXECABORT Transaction discarded because of previous errors." + ], + "notes": [ + "queued unknown command", + "runtime error in EXEC: no abort, reply array", + "wrong arity while queuing" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: EXECABORT Transaction discarded because of previous errors." + ] + }, + "noscript": { + "raw": [ + "-NOSCRIPT No matching script. Please use EVAL.", + "-NOSCRIPT No matching script. Please use EVAL." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: NOSCRIPT No matching script. Please use EVAL." + ] + }, + "busykey": { + "raw": [ + "-BUSYKEY Target key name already exists." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: BUSYKEY Target key name already exists." + ], + "notes": [ + "RESTORE REPLACE -> +OK" + ] + }, + "busygroup": { + "notes": [ + "first create -> +OK" + ], + "raw": [ + "-BUSYGROUP Consumer Group name already exists" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: BUSYGROUP Consumer Group name already exists" + ] + }, + "nogroup": { + "raw": [ + "-NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option", + "-NOGROUP No such key 'missing' or consumer group 'g' in XREADGROUP with GROUP option", + "-NOGROUP No such consumer group 'nog' for key name 's'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-NOGROUP No such key 's' or consumer group 'nog'", + "-ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", + "-NOGROUP No such consumer group 'nog' for key name 's'" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: NOGROUP No such key 's' or consumer group 'nog' in XREADGROUP with GROUP option" + ], + "notes": [ + "missing key", + "create on missing key without MKSTREAM" + ] + }, + "wrongpass": { + "raw": [ + "-ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled.", + "-WRONGPASS invalid username-password pair or user is disabled." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) SimpleError: WRONGPASS invalid username-password pair or user is disabled. [inner StackExchange.Redis.RedisServerException: SimpleError: WRONGPASS invalid username-password pair or user is disabled.]" + ], + "notes": [ + "AUTH when no password configured", + "unknown user", + "disabled user" + ] + }, + "oom": { + "raw": [ + "-OOM command not allowed when used memory > 'maxmemory'." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: OOM command not allowed when used memory > 'maxmemory'." + ], + "notes": [ + "GET while over maxmemory -> b'v'", + "DEL while over maxmemory -> :1", + "policy -> [b'maxmemory-policy', b'noeviction']", + "volatile-lru SET with no TTL keys -> -OOM command not allowed when used memory > 'maxmemory'.", + "allkeys-lru SET -> +OK" + ] + }, + "busy": { + "raw": [ + "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "-BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code." + ], + "notes": [ + "PING during busy -> -BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "SCRIPT KILL -> +OK", + "script caller got -> -ERR Script killed by user with SCRIPT KILL... script: 694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7, on @user_script:1.", + "function", + "SCRIPT KILL on function -> -BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.", + "FUNCTION KILL -> +OK", + "function caller got -> -ERR Script killed by user with SCRIPT KILL... script: spin, on @user_function:2.", + "SCRIPT KILL after a write -> -UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.", + "SHUTDOWN NOSAVE -> None" + ] + }, + "err-max-clients": { + "raw": [ + "-ERR max number of clients reached" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code." + ] + }, + "noreplicas": { + "raw": [ + "-NOREPLICAS Not enough good replicas to write." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: NOREPLICAS Not enough good replicas to write." + ], + "notes": [ + "GET -> $-1" + ] + }, + "misconf": { + "raw": [ + "-MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + ], + "notes": [ + "GET -> $-1", + [ + "rdb_bgsave_in_progress:0", + "rdb_last_bgsave_status:err", + "rdb_last_bgsave_time_sec:1", + "rdb_current_bgsave_time_sec:-1" + ], + "log -> Failed opening the temp RDB file temp-107.rdb (in server root dir /tmp/rerr/ro) for saving: Permission denied" + ] + }, + "noauth": { + "raw": [ + "-NOAUTH Authentication required." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) NOAUTH Returned - connection has not yet authenticated [inner StackExchange.Redis.RedisServerException: NOAUTH Returned - connection has not yet authenticated]" + ], + "notes": [ + "PING before AUTH -> -NOAUTH Authentication required." + ] + }, + "noperm": { + "raw": [ + "-NOPERM User app has no permissions to run the 'set' command", + "-NOPERM No permissions to access a key" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code.", + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code." + ], + "notes": [ + [ + { + "reason": "command", + "object": "echo", + "username": "app" + }, + { + "reason": "key", + "object": "__Booksleeve_TieBreak", + "username": "app" + } + ] + ] + }, + "denied": { + "raw": [ + "-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code." + ] + }, + "readonly": { + "raw": [ + "-READONLY You can't write against a read only replica." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisConnectionException: No connection (requires writable - not eligible for replica) is active/available to service this operation: SET, mc: 1/1/0, clientName: VFJ1GQNV29-Andrew-Gareth-Stark(SE.Redis-v3.0.0.11234), IOCP: (Busy=0,Free=1000,Min=1,Max=1000), WORKER: (Busy=0,Free=32767,Min=8,Max=32767), POOL: (Threads=3,QueuedItems=0,CompletedItems=11,Timers=1), v: 3.0.0.11234" + ], + "notes": [ + "GET on replica -> $-1", + "replica-read-only -> [b'replica-read-only', b'yes']" + ] + }, + "masterdown": { + "raw": [ + "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + ], + "notes": [ + [ + "master_host:127.0.0.1", + "master_port:6499", + "master_link_status:down" + ] + ] + }, + "loading": { + "raw": [ + "-LOADING Redis is loading the dataset in memory" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: LOADING Redis is loading the dataset in memory" + ], + "notes": [ + "PING while loading -> -LOADING Redis is loading the dataset in memory", + [ + "loading:1", + "loading_start_time:1793718534", + "loading_total_bytes:667878", + "loading_rdb_used_mem:2696480", + "loading_loaded_bytes:8185", + "loading_loaded_perc:1.23", + "loading_eta_seconds:80" + ] + ] + }, + "moved": { + "raw": [ + "-MOVED 12182 127.0.0.1:6573" + ], + "se_redis_sync": [ + "no error" + ] + }, + "crossslot": { + "raw": [ + "-CROSSSLOT Keys in request don't hash to the same slot", + "-CROSSSLOT Keys in request don't hash to the same slot" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: CROSSSLOT Keys in request don't hash to the same slot" + ], + "notes": [ + "KEYSLOT user:1 :10778 user:2 :6777 {user}:1 :5474 {user}:2 :5474", + "MSET {user}:1 {user}:2 on owner -> +OK" + ] + }, + "ask": { + "raw": [ + "-ASK 12182 127.0.0.1:6571" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisConnectionException: Endpoint 127.0.0.1:6571 serving hashslot 12182 is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. IOCP: (Busy=0,Free=1000,Min=1,Max=1000), WORKER: (Busy=1,Free=32766,Min=8,Max=32767), POOL: (Threads=4,QueuedItems=0,CompletedItems=38,Timers=1)" + ], + "notes": [ + "GET existing key on source -> b'1'", + "GET on target without ASKING -> -MOVED 12182 127.0.0.1:6573", + "ASKING then GET on target -> +OK $-1" + ] + }, + "tryagain": { + "raw": [ + "-TRYAGAIN Multiple keys request during rehashing of slot" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: TRYAGAIN Multiple keys request during rehashing of slot" + ] + }, + "clusterdown": { + "raw": [ + "-CLUSTERDOWN Hash slot not served", + "-CLUSTERDOWN The cluster is down" + ], + "se_redis_sync": [ + "StackExchange.Redis.RedisServerException: CLUSTERDOWN Hash slot not served", + "StackExchange.Redis.RedisServerException: CLUSTERDOWN The cluster is down" + ], + "notes": [ + "CLUSTER INFO state -> ['cluster_state:fail']" + ] + } +} diff --git a/content/develop/reference/errors/common/wrongtype.md b/content/develop/reference/errors/common/wrongtype.md index aa3a0df40f..c25954ba0a 100644 --- a/content/develop/reference/errors/common/wrongtype.md +++ b/content/develop/reference/errors/common/wrongtype.md @@ -20,6 +20,7 @@ error: - Operation against a key holding the wrong kind of value - Existing key has wrong Redis type - Key is not a valid HyperLogLog string value + - WrongTypeError --- Redis returns this error when you run a command on a key that holds a different data type than the command works with. Each key holds one data type, such as a string, a list, or a hash, and each command works with only one of them. diff --git a/data/client-exceptions.json b/data/client-exceptions.json index a3f5fa7910..808bd8e975 100644 --- a/data/client-exceptions.json +++ b/data/client-exceptions.json @@ -5,14 +5,24 @@ "jedis": "8.0.2", "node-redis": "6.3.1", "lettuce": "7.9.0.RELEASE", - "go-redis": "9.22.0" + "go-redis": "9.22.0", + "ioredis": "5.11.1", + "stackexchange-redis": "3.0.0", + "predis": "3.5.1", + "redis-rb": "6.0.0", + "redis-rs": "1.6.0" }, "order": [ "redis-py", "jedis", "node-redis", "lettuce", - "go-redis" + "go-redis", + "ioredis", + "stackexchange-redis", + "predis", + "redis-rb", + "redis-rs" ], "clients": { "redis-py": { @@ -45,6 +55,38 @@ "package": "github.com/redis/go-redis/v9", "generic": "redis.Error", "basis": "live" + }, + "ioredis": { + "name": "ioredis", + "package": "ioredis", + "generic": "ReplyError", + "basis": "live" + }, + "stackexchange-redis": { + "name": "StackExchange.Redis", + "package": "StackExchange.Redis", + "generic": "RedisServerException", + "basis": "live", + "note": "NRedisStack uses StackExchange.Redis, so it raises the same exceptions. The sync and async APIs raise the same exceptions." + }, + "predis": { + "name": "Predis", + "package": "Predis", + "generic": "Predis\\Response\\ServerException", + "basis": "live" + }, + "redis-rb": { + "name": "redis-rb", + "package": "Redis", + "generic": "Redis::CommandError", + "basis": "live" + }, + "redis-rs": { + "name": "redis-rs", + "package": "redis", + "generic": "ErrorKind::Extension", + "basis": "live", + "note": "The sync and async APIs raise the same errors." } }, "errors": { @@ -75,6 +117,24 @@ "message": "ASK :", "check": "redis.IsAskError(err)", "note": "`ClusterClient` follows the redirect itself, and returns this error after `MaxRedirects` attempts (3 by default)." + }, + "ioredis": { + "class": "ReplyError", + "message": "ASK :", + "check": "err.message.startsWith('ASK')" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "ASK :", + "check": "$e->getErrorType() === 'ASK'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "ASK : (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::Ask)", + "message": "Ask: :" } }, "busy": { @@ -99,6 +159,30 @@ "class": "redis.Error", "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", "check": "redis.HasErrorPrefix(err, \"BUSY\")" + }, + "ioredis": { + "class": "ReplyError", + "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "check": "err.message.startsWith('BUSY')" + }, + "stackexchange-redis": { + "class": "RedisConnectionException", + "message": "It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code.", + "note": "StackExchange.Redis sends commands when it connects, and the server refused them while a script ran, so a new connection fails." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "check": "$e->getErrorType() === 'BUSY'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Extension", + "message": "\"BUSY\": Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.", + "check": "err.code() == Some(\"BUSY\")" } }, "busygroup": { @@ -123,6 +207,29 @@ "class": "redis.Error", "message": "BUSYGROUP Consumer Group name already exists", "check": "redis.HasErrorPrefix(err, \"BUSYGROUP\")" + }, + "ioredis": { + "class": "ReplyError", + "message": "BUSYGROUP Consumer Group name already exists", + "check": "err.message.startsWith('BUSYGROUP')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "BUSYGROUP Consumer Group name already exists" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "BUSYGROUP Consumer Group name already exists", + "check": "$e->getErrorType() === 'BUSYGROUP'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "BUSYGROUP Consumer Group name already exists (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Extension", + "message": "\"BUSYGROUP\": Consumer Group name already exists", + "check": "err.code() == Some(\"BUSYGROUP\")" } }, "busykey": { @@ -147,6 +254,29 @@ "class": "redis.Error", "message": "BUSYKEY Target key name already exists.", "check": "redis.HasErrorPrefix(err, \"BUSYKEY\")" + }, + "ioredis": { + "class": "ReplyError", + "message": "BUSYKEY Target key name already exists.", + "check": "err.message.startsWith('BUSYKEY')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "BUSYKEY Target key name already exists." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "BUSYKEY Target key name already exists.", + "check": "$e->getErrorType() === 'BUSYKEY'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "BUSYKEY Target key name already exists. (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Extension", + "message": "\"BUSYKEY\": Target key name already exists.", + "check": "err.code() == Some(\"BUSYKEY\")" } }, "clusterdown": { @@ -171,6 +301,28 @@ "class": "redis.Error", "message": "CLUSTERDOWN The cluster is down", "check": "redis.IsClusterDownError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "CLUSTERDOWN The cluster is down", + "check": "err.message.startsWith('CLUSTERDOWN')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "CLUSTERDOWN The cluster is down" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "CLUSTERDOWN The cluster is down", + "check": "$e->getErrorType() === 'CLUSTERDOWN'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "CLUSTERDOWN The cluster is down (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::ClusterDown)", + "message": "ClusterDown: The cluster is down" } }, "crossslot": { @@ -195,6 +347,28 @@ "class": "redis.Error", "message": "CROSSSLOT Keys in request don't hash to the same slot", "check": "errors.Is(err, redis.ErrCrossSlot)" + }, + "ioredis": { + "class": "ReplyError", + "message": "CROSSSLOT Keys in request don't hash to the same slot", + "check": "err.message.startsWith('CROSSSLOT')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "CROSSSLOT Keys in request don't hash to the same slot" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "CROSSSLOT Keys in request don't hash to the same slot", + "check": "$e->getErrorType() === 'CROSSSLOT'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "CROSSSLOT Keys in request don't hash to the same slot (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::CrossSlot)", + "message": "CrossSlot: Keys in request don't hash to the same slot" } }, "denied": { @@ -220,6 +394,29 @@ "class": "io.EOF", "message": "EOF", "note": "The server closes the connection after it sends the error, and go-redis reported `EOF` in our run, not the error text." + }, + "ioredis": { + "class": "Error", + "message": "Connection is closed.", + "note": "The server closes the connection after it sends the error." + }, + "stackexchange-redis": { + "class": "RedisConnectionException", + "message": "It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code.", + "note": "`Connect` raises this error when the server refuses the connection." + }, + "predis": { + "class": "Predis\\Connection\\ConnectionException", + "message": "Stream is already at the end [tcp://:]" + }, + "redis-rb": { + "class": "Redis::CannotConnectError", + "message": "EOFError (redis://:) (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Io", + "message": "unexpected end of file", + "note": "The server closes the connection after it sends the error. The async API reports `broken pipe`." } }, "err-max-clients": { @@ -244,6 +441,29 @@ "class": "io.EOF", "message": "EOF", "note": "The server closes the connection after it sends the error, and go-redis reported `EOF` in our run, not the error text." + }, + "ioredis": { + "class": "Error", + "message": "Connection is closed.", + "note": "The server closes the connection after it sends the error." + }, + "stackexchange-redis": { + "class": "RedisConnectionException", + "message": "It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code.", + "note": "`Connect` raises this error when the server refuses the connection." + }, + "predis": { + "class": "Predis\\Connection\\ConnectionException", + "message": "Stream is already at the end [tcp://:]" + }, + "redis-rb": { + "class": "Redis::CannotConnectError", + "message": "EOFError (redis://:) (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Io", + "message": "unexpected end of file", + "note": "The server closes the connection after it sends the error. The async API reports `broken pipe`." } }, "err-not-an-integer": { @@ -266,6 +486,26 @@ "go-redis": { "class": "redis.Error", "message": "ERR value is not an integer or out of range" + }, + "ioredis": { + "class": "ReplyError", + "message": "ERR value is not an integer or out of range" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "ERR value is not an integer or out of range" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "ERR value is not an integer or out of range" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "ERR value is not an integer or out of range (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::ResponseError)", + "message": "ResponseError: value is not an integer or out of range" } }, "err-syntax-error": { @@ -288,6 +528,26 @@ "go-redis": { "class": "redis.Error", "message": "ERR syntax error" + }, + "ioredis": { + "class": "ReplyError", + "message": "ERR syntax error" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "ERR syntax error" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "ERR syntax error" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "ERR syntax error (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::ResponseError)", + "message": "ResponseError: syntax error" } }, "err-unknown-command": { @@ -310,6 +570,26 @@ "go-redis": { "class": "redis.Error", "message": "ERR unknown command '', with args beginning with: " + }, + "ioredis": { + "class": "ReplyError", + "message": "ERR unknown command '', with args beginning with: " + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "ERR unknown command '', with args beginning with: " + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "ERR unknown command '', with args beginning with: " + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "ERR unknown command '', with args beginning with: (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::ResponseError)", + "message": "ResponseError: unknown command '', with args beginning with: " } }, "err-wrong-number-of-arguments": { @@ -332,6 +612,26 @@ "go-redis": { "class": "redis.Error", "message": "ERR wrong number of arguments for '' command" + }, + "ioredis": { + "class": "ReplyError", + "message": "ERR wrong number of arguments for '' command" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "ERR wrong number of arguments for '' command" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "ERR wrong number of arguments for '' command" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "ERR wrong number of arguments for '' command (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::ResponseError)", + "message": "ResponseError: wrong number of arguments for '' command" } }, "execabort": { @@ -358,6 +658,29 @@ "class": "redis.Error", "message": "EXECABORT Transaction discarded because of previous errors.", "check": "redis.IsExecAbortError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "EXECABORT Transaction discarded because of previous errors.", + "check": "err.message.startsWith('EXECABORT')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "EXECABORT Transaction discarded because of previous errors." + }, + "predis": { + "class": "Predis\\Transaction\\AbortedMultiExecException", + "message": "ERR unknown command ''", + "note": "`transaction()` throws with the error of the command that failed to queue, not `EXECABORT`." + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "ERR unknown command '' (redis://:)", + "note": "`multi` raises the error of the command that failed to queue, not `EXECABORT`." + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::ExecAbort)", + "message": "Transaction aborted: [(Index , error: )]" } }, "loading": { @@ -382,6 +705,29 @@ "class": "redis.Error", "message": "LOADING Redis is loading the dataset in memory", "check": "redis.IsLoadingError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "LOADING Redis is loading the dataset in memory", + "check": "err.message.startsWith('LOADING')", + "note": "By default ioredis waits while the server loads its data, so the command doesn't fail. Set `enableReadyCheck: false` to get this error." + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "LOADING Redis is loading the dataset in memory" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "LOADING Redis is loading the dataset in memory", + "check": "$e->getErrorType() === 'LOADING'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "LOADING Redis is loading the dataset in memory (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::BusyLoading)", + "message": "BusyLoading: Redis is loading the dataset in memory" } }, "masterdown": { @@ -406,6 +752,28 @@ "class": "redis.Error", "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.", "check": "redis.IsMasterDownError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.", + "check": "err.message.startsWith('MASTERDOWN')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.", + "check": "$e->getErrorType() === 'MASTERDOWN'" + }, + "redis-rb": { + "class": "Redis::ConnectionError", + "message": "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::MasterDown)", + "message": "MasterDown: Link with MASTER is down and replica-serve-stale-data is set to 'no'." } }, "misconf": { @@ -430,6 +798,29 @@ "class": "redis.Error", "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.", "check": "redis.HasErrorPrefix(err, \"MISCONF\")" + }, + "ioredis": { + "class": "ReplyError", + "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.", + "check": "err.message.startsWith('MISCONF')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.", + "check": "$e->getErrorType() === 'MISCONF'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Extension", + "message": "\"MISCONF\": Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.", + "check": "err.code() == Some(\"MISCONF\")" } }, "moved": { @@ -459,6 +850,29 @@ "message": "MOVED :", "check": "redis.IsMovedError(err)", "note": "`ClusterClient` follows the redirect itself, and returns this error after `MaxRedirects` attempts (3 by default)." + }, + "ioredis": { + "class": "ReplyError", + "message": "MOVED :", + "check": "err.message.startsWith('MOVED')" + }, + "stackexchange-redis": { + "class": "(none)", + "message": "", + "note": "When it connects to a cluster, StackExchange.Redis learns which node holds each slot and sends the command there, so no `MOVED` error reaches your code." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "MOVED :", + "check": "$e->getErrorType() === 'MOVED'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "MOVED : (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::Moved)", + "message": "Moved: :" } }, "noauth": { @@ -479,12 +893,37 @@ "lettuce": { "class": "RedisConnectionException", "message": "Unable to connect to /:", - "note": "The cause is a `RedisCommandExecutionException` with the message `NOAUTH HELLO must be called with the client already authenticated\u2026`. Lettuce authenticates and sends `HELLO` when it connects, so `connect()` raises this error." + "note": "The cause is a `RedisCommandExecutionException` with the message `NOAUTH HELLO must be called with the client already authenticated…`. Lettuce authenticates and sends `HELLO` when it connects, so `connect()` raises this error." }, "go-redis": { "class": "redis.Error", "message": "NOAUTH Authentication required.", "check": "redis.IsAuthError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "NOAUTH Authentication required.", + "check": "err.message.startsWith('NOAUTH')" + }, + "stackexchange-redis": { + "class": "RedisConnectionException", + "message": "It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) NOAUTH Returned - connection has not yet authenticated", + "note": "StackExchange.Redis authenticates when it connects, so `Connect` raises this error." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "NOAUTH Authentication required.", + "check": "$e->getErrorType() === 'NOAUTH'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH option can be used to authenticate the client and select the RESP protocol version at the same time (redis://:)", + "note": "redis-rb sends `HELLO` when it connects, so the message names `HELLO`." + }, + "redis-rs": { + "class": "ErrorKind::Extension", + "message": "\"NOAUTH\": Authentication required.", + "check": "err.code() == Some(\"NOAUTH\")" } }, "nogroup": { @@ -509,6 +948,29 @@ "class": "redis.Error", "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option", "check": "redis.HasErrorPrefix(err, \"NOGROUP\")" + }, + "ioredis": { + "class": "ReplyError", + "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option", + "check": "err.message.startsWith('NOGROUP')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option", + "check": "$e->getErrorType() === 'NOGROUP'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "NOGROUP No such key '' or consumer group '' in XREADGROUP with GROUP option (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Extension", + "message": "\"NOGROUP\": No such key '' or consumer group '' in XREADGROUP with GROUP option", + "check": "err.code() == Some(\"NOGROUP\")" } }, "noperm": { @@ -533,6 +995,29 @@ "class": "redis.Error", "message": "NOPERM User has no permissions to run the '' command", "check": "redis.IsPermissionError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "NOPERM User has no permissions to run the '' command", + "check": "err.message.startsWith('NOPERM')" + }, + "stackexchange-redis": { + "class": "RedisConnectionException", + "message": "It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code.", + "note": "StackExchange.Redis sends commands when it connects, and the ACL rules of the user in this example refused them." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "NOPERM User has no permissions to run the '' command", + "check": "$e->getErrorType() === 'NOPERM'" + }, + "redis-rb": { + "class": "Redis::PermissionError", + "message": "NOPERM User has no permissions to run the '' command (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::NoPerm)", + "message": "NoPerm: User has no permissions to run the '' command" } }, "noreplicas": { @@ -557,6 +1042,29 @@ "class": "redis.Error", "message": "NOREPLICAS Not enough good replicas to write.", "check": "redis.IsNoReplicasError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "NOREPLICAS Not enough good replicas to write.", + "check": "err.message.startsWith('NOREPLICAS')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "NOREPLICAS Not enough good replicas to write." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "NOREPLICAS Not enough good replicas to write.", + "check": "$e->getErrorType() === 'NOREPLICAS'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "NOREPLICAS Not enough good replicas to write. (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Extension", + "message": "\"NOREPLICAS\": Not enough good replicas to write.", + "check": "err.code() == Some(\"NOREPLICAS\")" } }, "noscript": { @@ -581,6 +1089,28 @@ "class": "redis.Error", "message": "NOSCRIPT No matching script. Please use EVAL.", "check": "errors.Is(err, redis.ErrNoScript)" + }, + "ioredis": { + "class": "ReplyError", + "message": "NOSCRIPT No matching script. Please use EVAL.", + "check": "err.message.startsWith('NOSCRIPT')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "NOSCRIPT No matching script. Please use EVAL." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "NOSCRIPT No matching script. Please use EVAL.", + "check": "$e->getErrorType() === 'NOSCRIPT'" + }, + "redis-rb": { + "class": "Redis::NoScriptError", + "message": "NOSCRIPT No matching script. Please use EVAL. (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::NoScript)", + "message": "NoScript: No matching script. Please use EVAL." } }, "oom": { @@ -605,6 +1135,29 @@ "class": "redis.Error", "message": "OOM command not allowed when used memory > 'maxmemory'.", "check": "redis.IsOOMError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "OOM command not allowed when used memory > 'maxmemory'.", + "check": "err.message.startsWith('OOM')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "OOM command not allowed when used memory > 'maxmemory'." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "OOM command not allowed when used memory > 'maxmemory'.", + "check": "$e->getErrorType() === 'OOM'" + }, + "redis-rb": { + "class": "Redis::OutOfMemoryError", + "message": "OOM command not allowed when used memory > 'maxmemory'. (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Extension", + "message": "\"OOM\": command not allowed when used memory > 'maxmemory'.", + "check": "err.code() == Some(\"OOM\")" } }, "readonly": { @@ -629,6 +1182,29 @@ "class": "redis.Error", "message": "READONLY You can't write against a read only replica.", "check": "redis.IsReadOnlyError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "READONLY You can't write against a read only replica.", + "check": "err.message.startsWith('READONLY')" + }, + "stackexchange-redis": { + "class": "RedisConnectionException", + "message": "No connection (requires writable - not eligible for replica) is active/available to service this operation: SET, …", + "note": "StackExchange.Redis refuses a write to a replica itself, so the server's `READONLY` error never arrives." + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "READONLY You can't write against a read only replica.", + "check": "$e->getErrorType() === 'READONLY'" + }, + "redis-rb": { + "class": "Redis::ReadOnlyError", + "message": "READONLY You can't write against a read only replica. (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::ReadOnly)", + "message": "ReadOnly: You can't write against a read only replica." } }, "tryagain": { @@ -653,6 +1229,28 @@ "class": "redis.Error", "message": "TRYAGAIN Multiple keys request during rehashing of slot", "check": "redis.IsTryAgainError(err)" + }, + "ioredis": { + "class": "ReplyError", + "message": "TRYAGAIN Multiple keys request during rehashing of slot", + "check": "err.message.startsWith('TRYAGAIN')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "TRYAGAIN Multiple keys request during rehashing of slot" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "TRYAGAIN Multiple keys request during rehashing of slot", + "check": "$e->getErrorType() === 'TRYAGAIN'" + }, + "redis-rb": { + "class": "Redis::CommandError", + "message": "TRYAGAIN Multiple keys request during rehashing of slot (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Server(ServerErrorKind::TryAgain)", + "message": "TryAgain: Multiple keys request during rehashing of slot" } }, "wrongpass": { @@ -663,7 +1261,7 @@ "jedis": { "class": "JedisAccessControlException", "message": "WRONGPASS invalid username-password pair or user is disabled.", - "note": "`AUTH` with a password for a user that has none returns `ERR AUTH called without any password configured for the default user\u2026`, which is a `JedisDataException`." + "note": "`AUTH` with a password for a user that has none returns `ERR AUTH called without any password configured for the default user…`, which is a `JedisDataException`." }, "node-redis": { "class": "SimpleError", @@ -673,13 +1271,39 @@ "lettuce": { "class": "RedisConnectionException", "message": "Unable to connect to /:", - "note": "The cause is a `RedisCommandExecutionException` with the server's `WRONGPASS` message. `AUTH` with a password for a user that has none raises `RedisCommandExecutionException` with the `ERR AUTH called without any password configured\u2026` message, and no `RedisConnectionException`." + "note": "The cause is a `RedisCommandExecutionException` with the server's `WRONGPASS` message. `AUTH` with a password for a user that has none raises `RedisCommandExecutionException` with the `ERR AUTH called without any password configured…` message, and no `RedisConnectionException`." }, "go-redis": { "class": "redis.Error", "message": "WRONGPASS invalid username-password pair or user is disabled.", "check": "redis.IsAuthError(err)", - "note": "`AUTH` with a password for a user that has none returns `ERR AUTH called without any password configured\u2026`, which has no check of its own." + "note": "`AUTH` with a password for a user that has none returns `ERR AUTH called without any password configured…`, which has no check of its own." + }, + "ioredis": { + "class": "ReplyError", + "message": "WRONGPASS invalid username-password pair or user is disabled.", + "check": "err.message.startsWith('WRONGPASS')", + "note": "`AUTH` with a password for a user that has none returns `ERR AUTH called without any password configured…`." + }, + "stackexchange-redis": { + "class": "RedisConnectionException", + "message": "It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) SimpleError: WRONGPASS invalid username-password pair or user is disabled.", + "note": "`AUTH` with a password for a user that has none raises `RedisServerException` with the message `ERR AUTH called without any password configured…`." + }, + "predis": { + "class": "Predis\\Connection\\ConnectionException", + "message": "Failed: WRONGPASS invalid username-password pair or user is disabled. [tcp://:]", + "note": "`AUTH` with a password for a user that has none raises `Predis\\Response\\ServerException` with the message `ERR AUTH called without any password configured…`." + }, + "redis-rb": { + "class": "Redis::CannotConnectError", + "message": "WRONGPASS invalid username-password pair or user is disabled. (redis://:)", + "note": "`AUTH` with a password for a user that has none raises `Redis::CommandError` with the message `ERR AUTH called without any password configured…`." + }, + "redis-rs": { + "class": "ErrorKind::AuthenticationFailed", + "message": "Password authentication failed - AuthenticationFailed", + "note": "`AUTH` with a password for a user that has none returns `ErrorKind::Server(ServerErrorKind::ResponseError)` with the message `ResponseError: AUTH called without any password configured…`." } }, "wrongtype": { @@ -704,6 +1328,29 @@ "class": "redis.Error", "message": "WRONGTYPE Operation against a key holding the wrong kind of value", "check": "redis.HasErrorPrefix(err, \"WRONGTYPE\")" + }, + "ioredis": { + "class": "ReplyError", + "message": "WRONGTYPE Operation against a key holding the wrong kind of value", + "check": "err.message.startsWith('WRONGTYPE')" + }, + "stackexchange-redis": { + "class": "RedisServerException", + "message": "WRONGTYPE Operation against a key holding the wrong kind of value" + }, + "predis": { + "class": "Predis\\Response\\ServerException", + "message": "WRONGTYPE Operation against a key holding the wrong kind of value", + "check": "$e->getErrorType() === 'WRONGTYPE'" + }, + "redis-rb": { + "class": "Redis::WrongTypeError", + "message": "WRONGTYPE Operation against a key holding the wrong kind of value (redis://:)" + }, + "redis-rs": { + "class": "ErrorKind::Extension", + "message": "\"WRONGTYPE\": Operation against a key holding the wrong kind of value", + "check": "err.code() == Some(\"WRONGTYPE\")" } } } diff --git a/layouts/partials/markdown-error-clients.html b/layouts/partials/markdown-error-clients.html index 0f79ac26c5..a6badb496c 100644 --- a/layouts/partials/markdown-error-clients.html +++ b/layouts/partials/markdown-error-clients.html @@ -25,8 +25,8 @@ {{- $md := "| Client library | Exception or error type | Message |\n|---|---|---|\n" -}} {{- range $rows -}} {{- $msg := replace .message "|" "\\|" -}} - {{- $msg = cond (in $msg "`") (printf "`` %s ``" $msg) (printf "`%s`" $msg) -}} - {{- with .note -}}{{- $msg = printf "%s %s" $msg . -}}{{- end -}} + {{- $msg = cond (eq $msg "") "" (cond (in $msg "`") (printf "`` %s ``" $msg) (printf "`%s`" $msg)) -}} + {{- with .note -}}{{- $msg = strings.TrimSpace (printf "%s %s" $msg .) -}}{{- end -}} {{- $type := printf "`%s`" .class -}} {{- with .check -}}{{- $type = printf "%s, test with `%s`" $type . -}}{{- end -}} {{- $client := .client -}} diff --git a/layouts/shortcodes/error-clients.html b/layouts/shortcodes/error-clients.html index b8d6151adf..b247052de4 100644 --- a/layouts/shortcodes/error-clients.html +++ b/layouts/shortcodes/error-clients.html @@ -19,10 +19,11 @@ {{- range $rows }} + {{- $row := . }} - + {{- end }} From 673ccc715aad67cea741e901b4d980f033680b02 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Tue, 3 Nov 2026 15:57:56 +0000 Subject: [PATCH 06/11] DOC-7253 Add a Possible errors block to the API methods tab of command pages Under the signatures for each client, the tab lists the error pages that data/errors.json ties to the command, with the exception or error type that client raises for each, linked to the page. No schema change to the 527 mapping files, and nothing to maintain per method. A mapping list on each client in data/client-exceptions.json turns the dropdown's client ids (redis_py, nredisstack_sync, php and so on) into entries of that file, so the block follows whatever clients the file covers. The block shows only what the data says about a command by name, and that is much less than it sounds. An error that all commands that use keys can return, such as WRONGTYPE, has a scope in errors.json and no list of commands, so GET shows nothing while JSON.GET shows WRONGTYPE. Filling the gap by scope would be wrong: it would show WRONGTYPE for EXISTS. Of the 404 commands that errors.json names, about 250 have at least one linked page, and the generic ones (any command, any write) are not in it. The block says other errors are not listed and links to the error reference. The block sits in each client's signature div, so the existing client switch shows and hides it, and a command with no mapping file (EXEC, for example) has no signatures and so no block. The index of command to pages is built once with partialCached from the same catalog the error list uses, so a message and its page cannot disagree with the error reference. Learned: command-level error data is sparse because the server data records scopes, not commands, for the most common errors, so a per-command block can only be as complete as errors.json and must not guess from scope Constraint: the block must only list errors that errors.json ties to the command by name, never errors inferred from a scope such as keys or write Directive: To make GET and similar commands show WRONGTYPE, add the commands to the error's list in data/errors.json (and update_errors.py), not a scope rule in the template Gaps: Checked in a browser on EVALSHA for redis-py; switching client in the dropdown was not clicked through, though the block lives inside each client's div Recheck: when errors.json gains per-command lists for the key-scoped errors, or when a client is added to client-exceptions.json Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- data/client-exceptions.json | 54 +++++++++++++++---- .../components/api-methods-errors.html | 39 ++++++++++++++ .../partials/components/api-methods-tab.html | 1 + layouts/partials/errors/command-index.html | 30 +++++++++++ 4 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 layouts/partials/components/api-methods-errors.html create mode 100644 layouts/partials/errors/command-index.html diff --git a/data/client-exceptions.json b/data/client-exceptions.json index 808bd8e975..a54398b39d 100644 --- a/data/client-exceptions.json +++ b/data/client-exceptions.json @@ -29,64 +29,98 @@ "name": "redis-py", "package": "redis.exceptions", "generic": "ResponseError", - "basis": "live" + "basis": "live", + "mapping": [ + "redis_py" + ] }, "jedis": { "name": "Jedis", "package": "redis.clients.jedis.exceptions", "generic": "JedisDataException", - "basis": "live" + "basis": "live", + "mapping": [ + "jedis" + ] }, "node-redis": { "name": "node-redis", "package": "redis", "generic": "SimpleError", - "basis": "live" + "basis": "live", + "mapping": [ + "node_redis" + ] }, "lettuce": { "name": "Lettuce", "package": "io.lettuce.core", "generic": "RedisCommandExecutionException", "basis": "live", - "note": "The async API wraps the exception in an `ExecutionException` (`get()`) or a `CompletionException` (`join()`)." + "note": "The async API wraps the exception in an `ExecutionException` (`get()`) or a `CompletionException` (`join()`).", + "mapping": [ + "lettuce_sync", + "lettuce_async", + "lettuce_reactive" + ] }, "go-redis": { "name": "go-redis", "package": "github.com/redis/go-redis/v9", "generic": "redis.Error", - "basis": "live" + "basis": "live", + "mapping": [ + "go-redis" + ] }, "ioredis": { "name": "ioredis", "package": "ioredis", "generic": "ReplyError", - "basis": "live" + "basis": "live", + "mapping": [ + "ioredis" + ] }, "stackexchange-redis": { "name": "StackExchange.Redis", "package": "StackExchange.Redis", "generic": "RedisServerException", "basis": "live", - "note": "NRedisStack uses StackExchange.Redis, so it raises the same exceptions. The sync and async APIs raise the same exceptions." + "note": "NRedisStack uses StackExchange.Redis, so it raises the same exceptions. The sync and async APIs raise the same exceptions.", + "mapping": [ + "nredisstack_sync", + "nredisstack_async" + ] }, "predis": { "name": "Predis", "package": "Predis", "generic": "Predis\\Response\\ServerException", - "basis": "live" + "basis": "live", + "mapping": [ + "php" + ] }, "redis-rb": { "name": "redis-rb", "package": "Redis", "generic": "Redis::CommandError", - "basis": "live" + "basis": "live", + "mapping": [ + "redis_rb" + ] }, "redis-rs": { "name": "redis-rs", "package": "redis", "generic": "ErrorKind::Extension", "basis": "live", - "note": "The sync and async APIs raise the same errors." + "note": "The sync and async APIs raise the same errors.", + "mapping": [ + "redis_rs_sync", + "redis_rs_async" + ] } }, "errors": { diff --git a/layouts/partials/components/api-methods-errors.html b/layouts/partials/components/api-methods-errors.html new file mode 100644 index 0000000000..f48fe54bbc --- /dev/null +++ b/layouts/partials/components/api-methods-errors.html @@ -0,0 +1,39 @@ +{{/* + Possible errors block of the API methods tab: the error pages that + data/errors.json ties to a command, with the exception or error type that + one client library raises for each (data/client-exceptions.json). + + Parameters: + - commandName: The Redis command name (e.g., "SET") + - mappingId: The mappingClientId of the client (e.g., "redis_py") + + Renders nothing when the command has no such errors, or the client isn't in + data/client-exceptions.json. A command such as GET can have none: the data + lists the commands of an error, and an error that all commands that use keys + return has no list. See layouts/partials/errors/command-index.html. +*/}} +{{ $data := index site.Data "client-exceptions" }} +{{ $mappingId := .mappingId }} +{{ $clientId := "" }} +{{ range $id, $c := $data.clients }} + {{ if in $c.mapping $mappingId }}{{ $clientId = $id }}{{ end }} +{{ end }} +{{ $pages := index (partialCached "errors/command-index.html" site "errors") (lower .commandName) }} +{{ if and $clientId $pages }} +
+

Possible errors

+
    + {{ range $key := $pages }} + {{ with index $data.errors $key }}{{ with index . $clientId }} + {{ $ent := . }} + {{ with site.GetPage (printf "/develop/reference/errors/common/%s" $key) }} +
  • {{ .LinkTitle }}: {{ if eq $ent.class "(none)" }}no exception{{ else }}{{ $ent.class }}{{ with $ent.check }} (test with {{ . }}){{ end }}{{ end }}
  • + {{ end }} + {{ end }}{{ end }} + {{ end }} +
+

+ {{ with site.GetPage "/develop/reference/errors" }}See Redis error messages for what each error means. Other errors, such as connection errors, aren't listed.{{ end }} +

+
+{{ end }} diff --git a/layouts/partials/components/api-methods-tab.html b/layouts/partials/components/api-methods-tab.html index dbf54183f5..ae49ab28c1 100644 --- a/layouts/partials/components/api-methods-tab.html +++ b/layouts/partials/components/api-methods-tab.html @@ -78,6 +78,7 @@ {{ else }}

No method signature available for this client.

{{ end }} + {{ partial "components/api-methods-errors.html" (dict "commandName" $commandName "mappingId" $mappingId) }} {{ $first = false }} {{ end }} diff --git a/layouts/partials/errors/command-index.html b/layouts/partials/errors/command-index.html new file mode 100644 index 0000000000..8ab094c523 --- /dev/null +++ b/layouts/partials/errors/command-index.html @@ -0,0 +1,30 @@ +{{- /* + Which common error pages each command can return, from data/errors.json. + + Returns a dict: lowercase command name -> sorted slice of error page names + (the file name without the extension, such as "wrongtype"). Only errors that + data/errors.json ties to the command by name count. An error that + "commands that use keys" can return, with no list of commands, doesn't: the + data can't say that GET returns it but EXISTS doesn't. + + Call it with partialCached "errors/command-index.html" site "errors". +*/ -}} +{{- $catalog := partialCached "errors/catalog.html" site "errors" -}} +{{- $idx := dict -}} +{{- range $e := $catalog.entries -}} + {{- with $e.fix -}} + {{- $page := path.Base (strings.TrimSuffix "/" .) -}} + {{- range $e.commands -}} + {{- $c := lower .name -}} + {{- $have := index $idx $c | default slice -}} + {{- if not (in $have $page) -}} + {{- $idx = merge $idx (dict $c ($have | append $page)) -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} +{{- $out := dict -}} +{{- range $c, $pages := $idx -}} + {{- $out = merge $out (dict $c (sort $pages)) -}} +{{- end -}} +{{- return $out -}} From e6f1262b0ab4ed174995cf795ca15c3845aa82f5 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Wed, 4 Nov 2026 09:44:24 +0000 Subject: [PATCH 07/11] DOC-7253 Keep the client exceptions from going stale: check them offline and refresh them in the update skill `update_errors.py check` now fails when data/client-exceptions.json has no entry for a common/ page, has an entry for a page that no longer exists, lists a client that isn't in `order` or `versions`, or names a class that the client's last live run (client_baselines/) didn't see. A class counts as seen when its last segment, with any trailing `)`, appears in the recorded reply, which absorbs the Rust `ErrorKind::Server(Ask)` versus `ServerErrorKind::Ask` spelling; "(none)" rows are skipped. Run against the 13 baselines this found no false positives, and it does catch a renamed class or a deleted page entry. `live_clients.py` now lists the pages whose reply differs from the saved baseline instead of overwriting it, and saves only with `--accept`. A run with `--only` writes to .errors-update/clients/ and is not compared, because a page such as wrongpass spans several steps and a partial run reports false differences. `/update-error-reference` gains a step that runs each bridge on the new image, updates the data by hand, and accepts the run. Learned: a partial `live_checks` run (--only) can't be compared per page, since pages span steps Constraint: client bridges share Docker ports 6550-6575, so run them one at a time Directive: don't add `--only` support to the compare path, run the full set for a baseline Recheck: the skill step is untested end to end on a real Redis release, run it at the next one Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- .../skills/update-error-reference/SKILL.md | 44 ++++++++++++++++- build/errors/README.md | 15 +++--- build/errors/live_clients.py | 47 ++++++++++++++++--- build/errors/update_errors.py | 46 ++++++++++++++++++ build/test_update_errors.py | 14 ++++++ 5 files changed, 151 insertions(+), 15 deletions(-) diff --git a/.claude/skills/update-error-reference/SKILL.md b/.claude/skills/update-error-reference/SKILL.md index 7c80335ae6..d0e450dc83 100644 --- a/.claude/skills/update-error-reference/SKILL.md +++ b/.claude/skills/update-error-reference/SKILL.md @@ -113,7 +113,46 @@ page's example and text. If a change would remove or soften a limitation that the page states, flag it to the user instead of changing it. Then save the new replies with `--accept`. -## 5. Finish +## 5. Recheck the client libraries + +Each error page has an "In client libraries" table, from +`data/client-exceptions.json`. A new Redis release can change what a client +raises, as can a new client release. `check` already fails when the data names a +class that the last run of a client didn't see, or when a page has no entry. This +step refreshes those runs. Skip it only if the user says so, and say in the +summary that you did. + +1. Read `versions` in `data/client-exceptions.json` and find each client's latest + release on its registry. Ask the user before you move a client to a new + major version, because its API may have changed. +2. Run each client's bridge on the new Redis image, one at a time, because they + share the Docker ports 6550-6575. Run them outside the sandbox, as they need + Docker, the network and local ports. Each takes about two minutes: + + ``` + python3 build/errors/live_clients.py --image redis: [--version ] + ``` + + The bridges are `jedis`, `lettuce_sync`, `lettuce_async`, `lettuce_reactive`, + `node_redis`, `ioredis`, `go_redis`, `se_redis_sync`, `se_redis_async`, + `predis`, `redis_rb`, `rust_sync` and `rust_async`. redis-py is covered by + step 4. `build/errors/README.md` lists the toolchain each one needs. If one + is missing, ask the user to install it rather than skipping the client. +3. Each run lists the pages where the client's reply, or the server's, differs + from `client_baselines/.json`. For each page, update the entry in + `data/client-exceptions.json`: the class, the message, `check` and `note`. + Update the client's version in `versions`. Then save the run by running the + same command with `--accept`. It refuses a run in which a step failed. +4. For a new page in `common/`, add an entry for every client in `order`, with + the live result. Use `"class": "(none)"` for a client that raises nothing. If + the page's error names a class that the lookup should match, extend the + page's `match` list. + +A few rows depend on timing or on a restricted user, such as `denied` and max +clients, and can differ between runs. Run those again before you change a page +for them. + +## 6. Finish Run `python3 build/errors/update_errors.py check` and `python3 build/test_update_errors.py`, both of which must pass. Then tell the @@ -123,6 +162,7 @@ user, in a few lines: - the new error codes, - which explanations are `inferred`, and why, - any upstream bugs you found, such as a message with the wrong code or a typo, -- the pages that `verify` flagged, and what you changed. +- the pages that `verify` flagged, and what you changed, +- the clients whose replies changed, and the client versions you moved. Don't commit or open a pull request unless the user asks. diff --git a/build/errors/README.md b/build/errors/README.md index 72f6a41331..25388714b2 100644 --- a/build/errors/README.md +++ b/build/errors/README.md @@ -118,8 +118,7 @@ The "In client libraries" table on each error page comes from another client library, run the same checks with that client: ``` -python3 build/errors/live_clients.py jedis -python3 build/errors/live_clients.py node_redis --only standalone +python3 build/errors/live_clients.py jedis --image redis:8.12.0 --version 8.0.3 ``` It needs Docker, like `verify`, and the client's tools: Java and Maven (`jedis`, @@ -128,10 +127,14 @@ It needs Docker, like `verify`, and the client's tools: Java and Maven (`jedis`, `se_redis_async`), PHP and Composer (`predis`), Ruby (`redis_rb`), and Rust (`rust_sync`, `rust_async`). Each client is a bridge in `bridges/`: a short Python module and a probe program in the client's language that runs one command -and prints the exception class and message. The run writes -`client_baselines/.json`. Copy the classes and messages that the pages -show into `data/client-exceptions.json` by hand, and add the client to its -`order` list. The data file says which clients were run live. +and prints the exception class and message. The run lists the pages where the +client's reply, or the server's, differs from `client_baselines/.json`. +Copy the classes and messages that changed into `data/client-exceptions.json` by +hand, and the client's version into its `versions`. Then save the run with +`--accept`, which needs a full run, so it can't be combined with `--only`. +`update_errors.py check` fails when the data names a class that the baselines +didn't see, and when a page has no entry. Run the clients one at a time, because +they share the Docker ports. To add a client, copy `bridges/jedis.py` and its probe, and make the probe print `OKvalue` or `EXCclassmessage`. The format is in diff --git a/build/errors/live_clients.py b/build/errors/live_clients.py index 396a8843b4..b469eda4c5 100644 --- a/build/errors/live_clients.py +++ b/build/errors/live_clients.py @@ -6,9 +6,11 @@ It reproduces the same errors on the same throwaway Docker container as live_checks.py, but sends each command through the client library of a bridge in bridges/. It records the exception class and message that the client raises, -under the bridge's KEY, in client_baselines/.json. It doesn't touch -live_baseline.json. data/client-exceptions.json is written by hand from the -result. +under the bridge's KEY, and lists the pages where they differ from +client_baselines/.json. With --accept it saves the new run as that +baseline. It doesn't touch live_baseline.json. data/client-exceptions.json is +written by hand from the result, and `update_errors.py check` fails when it +disagrees with the baselines. """ import argparse, importlib.util, json, os, sys from pathlib import Path @@ -32,9 +34,12 @@ def main(): ap.add_argument('client', help='a bridge in build/errors/bridges/, such as jedis or node_redis') ap.add_argument('--version', help='the client version (default: the bridge\'s)') ap.add_argument('--image', default='redis:8.10', help='the Redis Docker image (default: redis:8.10)') - ap.add_argument('--out', help='where to write the result (default: client_baselines/.json)') + ap.add_argument('--accept', action='store_true', help='save the run as client_baselines/.json') + ap.add_argument('--out', help='where to write the run (default: .errors-update/clients/.json)') ap.add_argument('--only', default='', help='comma-separated steps to run, such as standalone,cluster') a = ap.parse_args() + if a.only and a.accept: + sys.exit('--accept needs a full run, because a page can span several steps. Leave out --only.') bridge = load(a.client) version = a.version or bridge.VERSION @@ -51,12 +56,40 @@ def py_err(fn): lc.py_err = py_err lc.CLIENT_KEY = bridge.KEY lc.EXTRA_META = {bridge.KEY: version} - out = Path(a.out) if a.out else HERE / 'client_baselines' / f'{a.client}.json' - out.parent.mkdir(exist_ok=True) + baseline_path = HERE / 'client_baselines' / f'{a.client}.json' + out = Path(a.out) if a.out else baseline_path if a.accept else HERE.parents[1] / '.errors-update' / 'clients' / f'{a.client}.json' + out.parent.mkdir(parents=True, exist_ok=True) only = tuple(s for s in a.only.split(',') if s) + old = baseline_path.read_text() if baseline_path.exists() else None result = lc.run(a.image, str(out), only=only) bad = result.get('_failures') - print(f'\nWrote {out}' + (f'\nFAILED STEPS: {bad}' if bad else '')) + if bad: + if a.accept and old is not None: baseline_path.write_text(old) + sys.exit(f'\nFAILED STEPS: {bad}\nThe baseline wasn\'t saved.') + if only: + sys.exit(f'\nWrote {out}. A run with --only isn\'t compared, because a page can span several steps.') + changes = changed(json.loads(old), result, bridge.KEY) if old else [] + if old and not changes: + print(f'\nEvery reply is the same as in {baseline_path.name}.') + for page, what in changes: + print(f'- common/{page}.md: {what}') + if a.accept: + print(f'\nSaved {baseline_path}. Update data/client-exceptions.json to match, then run ' + 'python3 build/errors/update_errors.py check') + elif changes or old is None: + print(f'\nWrote {out}. Check data/client-exceptions.json, then save it with --accept.') + + +def changed(baseline, results, key): + """The pages whose server reply or whose client's reply differs: [(page, what changed)].""" + out = [] + for page in sorted((set(baseline) | set(results)) - {'_meta', '_failures'}): + if page not in baseline or page not in results: continue + for kind in ('raw', key): + a, b = baseline[page].get(kind, []), results[page].get(kind, []) + if lc._same(a) != lc._same(b): + out.append((page, f'{kind}: was {str(a)[:200]!r}, now {str(b)[:200]!r}')) + return out if __name__ == '__main__': diff --git a/build/errors/update_errors.py b/build/errors/update_errors.py index ebc625b3cb..0ffdf09cc6 100644 --- a/build/errors/update_errors.py +++ b/build/errors/update_errors.py @@ -681,6 +681,50 @@ def needs_section(module, group): return module in ('core', 'vectorset') or group.startswith('SEARCH_') +# Each bridge's baseline file, and the client of data/client-exceptions.json that it covers. +CLIENT_BASELINES = {'jedis': 'jedis', 'node_redis': 'node-redis', 'lettuce_sync': 'lettuce', + 'lettuce_async': 'lettuce', 'lettuce_reactive': 'lettuce', 'go_redis': 'go-redis', + 'ioredis': 'ioredis', 'se_redis_sync': 'stackexchange-redis', + 'se_redis_async': 'stackexchange-redis', 'predis': 'predis', 'redis_rb': 'redis-rb', + 'rust_sync': 'redis-rs', 'rust_async': 'redis-rs'} + + +def check_clients(): + """Problems in data/client-exceptions.json: a missing entry, or a class that the last live run didn't see.""" + problems = [] + path = ROOT / 'data' / 'client-exceptions.json' + data = json.loads(path.read_text()) + pages = {p.stem for p in (PAGES / 'common').glob('*.md') if p.name != '_index.md'} + for page in sorted(pages - set(data['errors'])): + problems.append(f'client-exceptions.json: no entry for common/{page}.md') + for page in sorted(set(data['errors']) - pages): + problems.append(f'client-exceptions.json: the entry {page} has no page in common/') + for client in data['order']: + if client not in data['clients'] or client not in data['versions']: + problems.append(f'client-exceptions.json: {client} is in "order" but not in "clients" or "versions"') + for page, row in data['errors'].items(): + for client in sorted(set(row) - set(data['order'])): + problems.append(f'client-exceptions.json: {page} has an entry for {client}, which isn\'t in "order"') + + # The names must be the ones that the last live run of each client recorded. + for bridge, client in CLIENT_BASELINES.items(): + baseline_path = Path(__file__).parent / 'client_baselines' / f'{bridge}.json' + if not baseline_path.exists(): + problems.append(f'client_baselines/{bridge}.json is missing, so {client} wasn\'t checked live') + continue + baseline = json.loads(baseline_path.read_text()) + for page, row in data['errors'].items(): + entry = row.get(client) + replies = [r for k, v in baseline.get(page, {}).items() if k not in ('raw', 'notes') for r in v] + if not entry or not replies or entry['class'] == '(none)': + continue + name = re.sub(r'\W+$', '', entry['class']).split('::')[-1].split('.')[-1] + if not any(name in r for r in replies): + problems.append(f'client-exceptions.json: {page}/{client} says {entry["class"]}, but the last ' + f'{bridge} run got {replies[0][:100]!r}') + return problems + + def check(data): """Problems that need fixing, and notes worth a look.""" problems, notes = [], [] @@ -739,6 +783,8 @@ def same(msg): if not any(same(e['message']) for e in messages): problems.append(f'common/{page.name}: no message in the data matches the page\'s message: {text}') + problems += check_clients() + # The versions that the pages name. index = (PAGES / 'all' / '_index.md').read_text() for name, version in data['versions'].items(): diff --git a/build/test_update_errors.py b/build/test_update_errors.py index be7626d23b..ef1d2526e4 100644 --- a/build/test_update_errors.py +++ b/build/test_update_errors.py @@ -267,5 +267,19 @@ def test_failures_and_lost_replies_are_changes(self): self.assertEqual([p for p, _ in self.lc.compare(baseline, results)], ['_failures', 'oom']) +class TestClientChecks(unittest.TestCase): + def test_changed_ignores_numbers_and_flags_names(self): + sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'errors', 'bridges')) + import live_clients + old = {'_meta': {}, 'moved': {'raw': ['-MOVED 1 h:6570'], 'jedis': ['E: MOVED 1 h:6570']}} + same = {'_meta': {}, 'moved': {'raw': ['-MOVED 2 h:6571'], 'jedis': ['E: MOVED 2 h:6571']}} + diff = {'_meta': {}, 'moved': {'raw': ['-MOVED 2 h:6571'], 'jedis': ['F: MOVED 2 h:6571']}} + self.assertEqual(live_clients.changed(old, same, 'jedis'), []) + self.assertEqual([p for p, _ in live_clients.changed(old, diff, 'jedis')], ['moved']) + + def test_the_committed_client_data_matches_the_baselines(self): + self.assertEqual(u.check_clients(), []) + + if __name__ == '__main__': unittest.main() From 47cedb886fc91b0c9f4f4a13bf276bab88bed18d Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Wed, 4 Nov 2026 13:20:49 +0000 Subject: [PATCH 08/11] DOC-7253 Add the generic client exception classes to the errors index The errors index now has a section, "Errors without their own exception class", with a table of the class that each client raises when it has no class of its own for an error. It is the `generic` field of data/client-exceptions.json, so it can't drift from the per-page tables. The `error-clients` shortcode takes `generic="true"` for it, and the Markdown/JSON expansion handles both forms. Checked in a full Hugo build: the HTML table, index.html.md and index.json all carry it, and the per-page tables are unchanged. Learned: a Hugo build in a git worktree fails on develop/clients jupyter-example pages until data/examples.json and the other ignored data/ files are linked in; HUGO_IGNOREFILES='["content/develop/clients/.*"]' skips them Directive: keep the generic table generated from client-exceptions.json, don't hand-write it in the page Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- content/develop/reference/errors/_index.md | 6 +++ layouts/partials/markdown-error-clients.html | 39 +++++++++++++------- layouts/shortcodes/error-clients.html | 25 +++++++++++-- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/content/develop/reference/errors/_index.md b/content/develop/reference/errors/_index.md index d0fd88f7a8..3f3197fdca 100644 --- a/content/develop/reference/errors/_index.md +++ b/content/develop/reference/errors/_index.md @@ -55,3 +55,9 @@ The [list of all error messages](/content/develop/reference/errors/all/_index.md - [JSON](/content/develop/reference/errors/all/json.md) - [Probabilistic data types](/content/develop/reference/errors/all/probabilistic.md) - [Time series](/content/develop/reference/errors/all/time-series.md) + +## Errors without their own exception class + +Most client libraries have their own exception class for only a few prefixes. They raise every other error as one general server-error class, and you tell the errors apart by the prefix in the message. This table shows that class for each client library. For the class of a specific error, see its [common error](/content/develop/reference/errors/common/_index.md) page. + +{{< error-clients generic="true" >}} diff --git a/layouts/partials/markdown-error-clients.html b/layouts/partials/markdown-error-clients.html index a6badb496c..18baa45241 100644 --- a/layouts/partials/markdown-error-clients.html +++ b/layouts/partials/markdown-error-clients.html @@ -2,8 +2,9 @@ Expand error-clients shortcodes for Markdown/JSON output. The HTML shortcode (layouts/shortcodes/error-clients.html) renders the - exceptions that client libraries raise for an error page. AI-facing output - needs the same table as Markdown. Both read the rows from + exceptions that client libraries raise for an error page, or, with + generic="true", the class they raise for any other error. AI-facing output + needs the same tables as Markdown. The per-page rows come from layouts/partials/errors/client-rows.html. Input: dict with: @@ -14,24 +15,36 @@ */ -}} {{- $content := .RawContent -}} -{{- $matches := findRE `\{\{<\s*error-clients\s*>\}\}` $content -}} +{{- $matches := findRE `\{\{<\s*error-clients(\s+generic="true")?\s*>\}\}` $content -}} {{- /* A generated page, such as a section without an _index.md, has no .File. */ -}} {{- $rows := slice -}} {{- if and $matches .Page.File -}} {{- $rows = partial "errors/client-rows.html" .Page.File.TranslationBaseName -}} {{- end -}} +{{- $data := index site.Data "client-exceptions" -}} {{- range $match := $matches -}} - {{- $md := "| Client library | Exception or error type | Message |\n|---|---|---|\n" -}} - {{- range $rows -}} - {{- $msg := replace .message "|" "\\|" -}} - {{- $msg = cond (eq $msg "") "" (cond (in $msg "`") (printf "`` %s ``" $msg) (printf "`%s`" $msg)) -}} - {{- with .note -}}{{- $msg = strings.TrimSpace (printf "%s %s" $msg .) -}}{{- end -}} - {{- $type := printf "`%s`" .class -}} - {{- with .check -}}{{- $type = printf "%s, test with `%s`" $type . -}}{{- end -}} - {{- $client := .client -}} - {{- with .clientNote -}}{{- $client = printf "%s. %s" $client . -}}{{- end -}} - {{- $md = printf "%s| %s | %s | %s |\n" $md $client $type $msg -}} + {{- $md := "" -}} + {{- if in $match "generic" -}} + {{- $md = "| Client library | Exception or error type |\n|---|---|\n" -}} + {{- range $id := $data.order -}} + {{- $client := index $data.clients $id -}} + {{- $type := printf "`%s`" $client.generic -}} + {{- with $client.note -}}{{- $type = printf "%s. %s" $type . -}}{{- end -}} + {{- $md = printf "%s| %s | %s |\n" $md $client.name $type -}} + {{- end -}} + {{- else -}} + {{- $md = "| Client library | Exception or error type | Message |\n|---|---|---|\n" -}} + {{- range $rows -}} + {{- $msg := replace .message "|" "\\|" -}} + {{- $msg = cond (eq $msg "") "" (cond (in $msg "`") (printf "`` %s ``" $msg) (printf "`%s`" $msg)) -}} + {{- with .note -}}{{- $msg = strings.TrimSpace (printf "%s %s" $msg .) -}}{{- end -}} + {{- $type := printf "`%s`" .class -}} + {{- with .check -}}{{- $type = printf "%s, test with `%s`" $type . -}}{{- end -}} + {{- $client := .client -}} + {{- with .clientNote -}}{{- $client = printf "%s. %s" $client . -}}{{- end -}} + {{- $md = printf "%s| %s | %s | %s |\n" $md $client $type $msg -}} + {{- end -}} {{- end -}} {{- $content = replace $content $match $md 1 -}} {{- end -}} diff --git a/layouts/shortcodes/error-clients.html b/layouts/shortcodes/error-clients.html index b247052de4..074a36842c 100644 --- a/layouts/shortcodes/error-clients.html +++ b/layouts/shortcodes/error-clients.html @@ -8,14 +8,32 @@ Usage, on a page in errors/common: {{< error-clients >}} - layouts/partials/markdown-error-clients.html renders the same table for the - Markdown output, through the same layouts/partials/errors/client-rows.html. + With generic="true" it lists the class that each client raises for an error + that it has no class of its own for, and works on any page: + {{< error-clients generic="true" >}} + + layouts/partials/markdown-error-clients.html renders the same tables for the + Markdown output. The per-page rows come from layouts/partials/errors/client-rows.html. */ -}} -{{- $rows := partial "errors/client-rows.html" .Page.File.TranslationBaseName -}} +{{- $generic := eq (.Get "generic") "true" -}} {{- $v := substr (md5 (os.ReadFile "static/css/errors.css")) 0 10 }}
Client libraryExceptionMessage
Client libraryException or error typeMessage
{{ .client }}{{ .class }}{{ .client }}{{ with .clientNote }}
{{ . | markdownify }}{{ end }}
{{ .class }}{{ with .check }}
Test with {{ . }}{{ end }}
{{ .message }}{{ with .note }}
{{ . | markdownify }}{{ end }}
Client libraryException or error typeMessage
{{ .client }}{{ with .clientNote }}
{{ . | markdownify }}{{ end }}
{{ .class }}{{ with .check }}
Test with {{ . }}{{ end }}
{{ .message }}{{ with .note }}
{{ . | markdownify }}{{ end }}
{{ with .message }}{{ . }}{{ end }}{{ with .note }}{{ if $row.message }}
{{ end }}{{ . | markdownify }}{{ end }}
+{{- if $generic }} + {{- $data := index site.Data "client-exceptions" }} + + + {{- range $id := $data.order }} + {{- $client := index $data.clients $id }} + + + + + {{- end }} + +{{- else }} + {{- $rows := partial "errors/client-rows.html" .Page.File.TranslationBaseName }} {{- range $rows }} @@ -27,5 +45,6 @@ {{- end }} +{{- end }}
Client libraryException or error type
{{ $client.name }}{{ $client.generic }}{{ with $client.note }}
{{ . | markdownify }}{{ end }}
Client libraryException or error typeMessage
From 985114fcd4ccffa9e0ec89d3b9ff181e67baeb68 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Wed, 4 Nov 2026 14:26:27 +0000 Subject: [PATCH 09/11] DOC-7253 Correct the Jedis exception hierarchy and make the ioredis loading check self-contained The Jedis error-handling page's hierarchy now matches jedis 8.0.2, read with javap from the jar. JedisClusterException extends JedisDataException, not JedisException. JedisConnectionException, JedisValidationException and JedisClusterOperationException extend JedisException directly. AbortedTransactionException isn't in 8.0.2 at all, so it is removed. The page now also lists JedisBusyException, JedisBroadcastException, JedisProtocolNotSupportedException, JedisCacheException, ClusterAggregationException and UnsupportedAggregationException. Bugbot found that the ioredis probe only turned its ready check off when IOREDIS_READY_CHECK=0 was set by hand, so a normal run of the loading step waited for the load to finish and didn't record ReplyError. A bridge can now declare STEP_ENV, and live_clients.py sets those variables for that step only. Run without the variable, ioredis now records ReplyError for loading. Learned: ioredis waits on LOADING by default, so its loading check needs enableReadyCheck:false, but other steps keep the default Constraint: AbortedTransactionException removal rests on the 8.0.2 jar, so recheck the hierarchy when the page's Jedis version moves Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- build/errors/bridges/ioredis.py | 2 ++ build/errors/live_clients.py | 23 ++++++++++++++++++- .../develop/clients/jedis/error-handling.md | 15 ++++++++---- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/build/errors/bridges/ioredis.py b/build/errors/bridges/ioredis.py index 93cbd557bd..61f5416c07 100644 --- a/build/errors/bridges/ioredis.py +++ b/build/errors/bridges/ioredis.py @@ -5,6 +5,8 @@ KEY = 'ioredis' VERSION = '5.11.1' +# ioredis waits while the server is loading its data, so the loading step turns its ready check off. +STEP_ENV = {'loading': {'IOREDIS_READY_CHECK': '0'}} HERE = Path(__file__).parent diff --git a/build/errors/live_clients.py b/build/errors/live_clients.py index b469eda4c5..d8899dec02 100644 --- a/build/errors/live_clients.py +++ b/build/errors/live_clients.py @@ -12,7 +12,7 @@ written by hand from the result, and `update_errors.py check` fails when it disagrees with the baselines. """ -import argparse, importlib.util, json, os, sys +import argparse, functools, importlib.util, json, os, sys from pathlib import Path HERE = Path(__file__).parent @@ -56,6 +56,12 @@ def py_err(fn): lc.py_err = py_err lc.CLIENT_KEY = bridge.KEY lc.EXTRA_META = {bridge.KEY: version} + # A bridge can set environment variables for a step, such as ioredis, which waits + # while the server is loading unless its ready check is off. + for i, step in enumerate(lc.STEPS): + env = getattr(bridge, 'STEP_ENV', {}).get(step.__name__) + if env: + lc.STEPS[i] = with_env(step, env) baseline_path = HERE / 'client_baselines' / f'{a.client}.json' out = Path(a.out) if a.out else baseline_path if a.accept else HERE.parents[1] / '.errors-update' / 'clients' / f'{a.client}.json' out.parent.mkdir(parents=True, exist_ok=True) @@ -80,6 +86,21 @@ def py_err(fn): print(f'\nWrote {out}. Check data/client-exceptions.json, then save it with --accept.') +def with_env(step, env): + """`step`, run with the environment variables `env` set.""" + @functools.wraps(step) + def run(): + old = {k: os.environ.get(k) for k in env} + os.environ.update(env) + try: + return step() + finally: + for k, v in old.items(): + if v is None: os.environ.pop(k, None) + else: os.environ[k] = v + return run + + def changed(baseline, results, key): """The pages whose server reply or whose client's reply differs: [(page, what changed)].""" out = [] diff --git a/content/develop/clients/jedis/error-handling.md b/content/develop/clients/jedis/error-handling.md index c610b420d0..86d6d7728b 100644 --- a/content/develop/clients/jedis/error-handling.md +++ b/content/develop/clients/jedis/error-handling.md @@ -31,13 +31,18 @@ Jedis organizes exceptions in a hierarchy rooted at `JedisException`, which exte "JedisRedirectionException": "JedisMovedDataException": "JedisAskDataException": - "AbortedTransactionException": "JedisAccessControlException": "JedisNoScriptException": - "JedisClusterException": - "JedisClusterOperationException": - "JedisConnectionException": - "JedisValidationException": + "JedisBusyException": + "JedisClusterException": + "JedisBroadcastException": + "JedisProtocolNotSupportedException": + "JedisClusterOperationException": + "ClusterAggregationException": + "UnsupportedAggregationException": + "JedisConnectionException": + "JedisValidationException": + "JedisCacheException": "InvalidURIException": ``` From e11ecd1e1e8cc4ad7e7e92dc43a03f60fde78fb5 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Wed, 4 Nov 2026 14:31:20 +0000 Subject: [PATCH 10/11] DOC-7253 Link the client error-handling pages to the error reference A reviewer suggested linking from the client-specific error-handling pages to the new error reference. Each of the nine client pages (redis-py, Jedis, node-redis and ioredis, Lettuce, go-redis, StackExchange.Redis, Predis, redis-rb, redis-rs) gets one sentence, just before "Applying error handling patterns", that names its client and links to the errors index. The overview page gets one sentence under its categories table. The index is the target rather than common/ because it has the lookup box as well as the common errors. Checked in a Hugo build with the clients section's failing pages excluded: the links render as /develop/reference/errors/. Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- content/develop/clients/dotnet/error-handling.md | 2 ++ content/develop/clients/error-handling.md | 2 ++ content/develop/clients/go/error-handling.md | 2 ++ content/develop/clients/jedis/error-handling.md | 2 ++ content/develop/clients/lettuce/error-handling.md | 2 ++ content/develop/clients/nodejs/error-handling.md | 2 ++ content/develop/clients/php/error-handling.md | 2 ++ content/develop/clients/redis-py/error-handling.md | 2 ++ content/develop/clients/ruby/error-handling.md | 2 ++ content/develop/clients/rust/error-handling.md | 2 ++ 10 files changed, 20 insertions(+) diff --git a/content/develop/clients/dotnet/error-handling.md b/content/develop/clients/dotnet/error-handling.md index 4c10a4af05..dffa3e20a3 100644 --- a/content/develop/clients/dotnet/error-handling.md +++ b/content/develop/clients/dotnet/error-handling.md @@ -36,6 +36,8 @@ app reliability. See [Categories of errors](/content/develop/clients/error-handling.md#categories-of-errors) for a more detailed discussion of these errors and their causes. +For the StackExchange.Redis exception that each common server error raises, see [Redis error messages](/content/develop/reference/errors/_index.md). + ## Applying error handling patterns The [Error handling](/content/develop/clients/error-handling.md) overview diff --git a/content/develop/clients/error-handling.md b/content/develop/clients/error-handling.md index 53217aafd1..2a184f9e95 100644 --- a/content/develop/clients/error-handling.md +++ b/content/develop/clients/error-handling.md @@ -31,6 +31,8 @@ Redis errors fall into four main categories. The table below provides a quick ov | [Data errors](#data-errors) | Serialization failures, corrupted data, type mismatches | Sometimes (depends on data source) | `JSONDecodeError`, `SerializationError`, `WRONGTYPE` | | [Resource errors](#resource-errors) | Memory limit, pool exhausted, too many connections, key eviction | Sometimes (some are temporary) | `OOM`, pool timeout, `LOADING` | +To look up a specific server error message, and the exception that each client library raises for it, see [Redis error messages](/content/develop/reference/errors/_index.md). + ### Connection errors Connection errors occur when your application cannot communicate with Redis. These are typically temporary and often recoverable. diff --git a/content/develop/clients/go/error-handling.md b/content/develop/clients/go/error-handling.md index 6c2d531937..ff7cfacea8 100644 --- a/content/develop/clients/go/error-handling.md +++ b/content/develop/clients/go/error-handling.md @@ -44,6 +44,8 @@ Common error types from go-redis include: See [Categories of errors](/content/develop/clients/error-handling.md#categories-of-errors) for a more detailed discussion of these errors and their causes. +For the go-redis exception that each common server error raises, see [Redis error messages](/content/develop/reference/errors/_index.md). + ## Applying error handling patterns The [Error handling](/content/develop/clients/error-handling.md) overview diff --git a/content/develop/clients/jedis/error-handling.md b/content/develop/clients/jedis/error-handling.md index 86d6d7728b..3180ef26e1 100644 --- a/content/develop/clients/jedis/error-handling.md +++ b/content/develop/clients/jedis/error-handling.md @@ -59,6 +59,8 @@ for a more detailed discussion of these errors and their causes. | `JedisDataException` | Problem with data being sent or received | ❌ | Fix the data or command | | `JedisException` | Unexpected errors (catch-all) | ❌ | Log and investigate | +For the Jedis exception that each common server error raises, see [Redis error messages](/content/develop/reference/errors/_index.md). + ## Applying error handling patterns The [Error handling](/content/develop/clients/error-handling.md) overview diff --git a/content/develop/clients/lettuce/error-handling.md b/content/develop/clients/lettuce/error-handling.md index 8944bbad04..b09a8e155b 100644 --- a/content/develop/clients/lettuce/error-handling.md +++ b/content/develop/clients/lettuce/error-handling.md @@ -57,6 +57,8 @@ for a more detailed discussion of these errors and their causes. | `RedisCommandExecutionException` | Redis returned an error reply such as `WRONGTYPE` | ❌ | Fix the command, arguments, or data model | | `RedisCommandInterruptedException` | A waiting thread was interrupted while waiting for a result | ⚠️ | Restore interrupt status and abort cleanly | +For the Lettuce exception that each common server error raises, see [Redis error messages](/content/develop/reference/errors/_index.md). + ## Applying error handling patterns The [Error handling](/content/develop/clients/error-handling.md) overview diff --git a/content/develop/clients/nodejs/error-handling.md b/content/develop/clients/nodejs/error-handling.md index 92900440df..cc76e554b5 100644 --- a/content/develop/clients/nodejs/error-handling.md +++ b/content/develop/clients/nodejs/error-handling.md @@ -91,6 +91,8 @@ client.on('error', error => { }); ``` +For the node-redis and ioredis exception that each common server error raises, see [Redis error messages](/content/develop/reference/errors/_index.md). + ## Applying error handling patterns The [Error handling](/content/develop/clients/error-handling.md) diff --git a/content/develop/clients/php/error-handling.md b/content/develop/clients/php/error-handling.md index a52b3d9c82..f3abe60811 100644 --- a/content/develop/clients/php/error-handling.md +++ b/content/develop/clients/php/error-handling.md @@ -54,6 +54,8 @@ for a more detailed discussion of these errors and their causes. | `Predis\Response\ServerException` | Redis returned an error reply such as `WRONGTYPE` | ❌ | Fix the command, arguments, or data model | | `Predis\Transaction\AbortedMultiExecException` | A watched transaction was aborted | ✅ | Reload state and retry the transaction | +For the Predis exception that each common server error raises, see [Redis error messages](/content/develop/reference/errors/_index.md). + ## Applying error handling patterns The [Error handling](/content/develop/clients/error-handling.md) overview diff --git a/content/develop/clients/redis-py/error-handling.md b/content/develop/clients/redis-py/error-handling.md index b740a2d635..79699d6aad 100644 --- a/content/develop/clients/redis-py/error-handling.md +++ b/content/develop/clients/redis-py/error-handling.md @@ -57,6 +57,8 @@ for a more detailed discussion of these errors and their causes. | `redis.ResponseError` | Invalid command or Redis error response | ❌ | Fix the command or arguments | | `redis.DataError` | Data serialization/deserialization issues | ⚠️ | Log, invalidate cache, fetch fresh data | +For the redis-py exception that each common server error raises, see [Redis error messages](/content/develop/reference/errors/_index.md). + ## Applying error handling patterns The [Error handling](/content/develop/clients/error-handling.md) overview diff --git a/content/develop/clients/ruby/error-handling.md b/content/develop/clients/ruby/error-handling.md index 37daebfc61..839f0b0bea 100644 --- a/content/develop/clients/ruby/error-handling.md +++ b/content/develop/clients/ruby/error-handling.md @@ -58,6 +58,8 @@ for a more detailed discussion of these errors and their causes. | `Redis::CommandError` | Redis returned an error reply such as `ERR` or `WRONGTYPE` | ❌ | Fix the command, arguments, or data model | | `Redis::ConnectionError` | An established connection was lost | ✅ | Reconnect and retry cautiously | +For the redis-rb exception that each common server error raises, see [Redis error messages](/content/develop/reference/errors/_index.md). + ## Applying error handling patterns The [Error handling](/content/develop/clients/error-handling.md) overview diff --git a/content/develop/clients/rust/error-handling.md b/content/develop/clients/rust/error-handling.md index 86f9b643cd..d024bbc1e1 100644 --- a/content/develop/clients/rust/error-handling.md +++ b/content/develop/clients/rust/error-handling.md @@ -51,6 +51,8 @@ redis-rs provides a `RedisError` type with various error kinds. Common error kin See [Categories of errors](/content/develop/clients/error-handling.md#categories-of-errors) for a more detailed discussion of these errors and their causes. +For the redis-rs exception that each common server error raises, see [Redis error messages](/content/develop/reference/errors/_index.md). + ## Applying error handling patterns The [Error handling](/content/develop/clients/error-handling.md) overview From 0c1aa16941fe430c7722a11ed80b10ae6013af6c Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Wed, 4 Nov 2026 14:53:05 +0000 Subject: [PATCH 11/11] DOC-7253 Check redis-py against its baseline too and report pages that a client run loses Two Bugbot findings on the client-exceptions checks, both valid. `update_errors.py check` compared the data with the bridge baselines only, so a wrong redis-py class passed, although redis-py is first in every table. It now also reads live_baseline.json, where `verify` records the redis-py names, and a test changes a redis-py class and expects a problem. `live_clients.py` skipped a page that was in the old baseline but not in the new run, so a refresh that lost a page printed a clean diff and --accept dropped the page. It now reports a page that the run lost, and one that has no baseline yet. Two other Bugbot comments say the ioredis loading step needs IOREDIS_READY_CHECK=0 set by hand. They were written against 985114fcd, which already has STEP_ENV, and a live run without the variable records ReplyError, so they are stale. Learned: Bugbot can review a commit and still flag what that commit fixes, so check the cited line before acting Ticket: DOC-7253 Co-Authored-By: Claude Sonnet 5.5 --- build/errors/live_clients.py | 7 ++++++- build/errors/update_errors.py | 7 +++++-- build/test_update_errors.py | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/build/errors/live_clients.py b/build/errors/live_clients.py index d8899dec02..6542ba9897 100644 --- a/build/errors/live_clients.py +++ b/build/errors/live_clients.py @@ -105,7 +105,12 @@ def changed(baseline, results, key): """The pages whose server reply or whose client's reply differs: [(page, what changed)].""" out = [] for page in sorted((set(baseline) | set(results)) - {'_meta', '_failures'}): - if page not in baseline or page not in results: continue + if page not in results: + out.append((page, 'no replies this time, so --accept would drop it from the baseline')) + continue + if page not in baseline: + out.append((page, 'no baseline yet. Check it by hand')) + continue for kind in ('raw', key): a, b = baseline[page].get(kind, []), results[page].get(kind, []) if lc._same(a) != lc._same(b): diff --git a/build/errors/update_errors.py b/build/errors/update_errors.py index 0ffdf09cc6..b734692a1e 100644 --- a/build/errors/update_errors.py +++ b/build/errors/update_errors.py @@ -707,10 +707,13 @@ def check_clients(): problems.append(f'client-exceptions.json: {page} has an entry for {client}, which isn\'t in "order"') # The names must be the ones that the last live run of each client recorded. + # redis-py is recorded by `verify`, in live_baseline.json, and the others by live_clients.py. + sources = {'redis_py': ('redis-py', Path(__file__).parent / 'live_baseline.json')} for bridge, client in CLIENT_BASELINES.items(): - baseline_path = Path(__file__).parent / 'client_baselines' / f'{bridge}.json' + sources[bridge] = (client, Path(__file__).parent / 'client_baselines' / f'{bridge}.json') + for bridge, (client, baseline_path) in sources.items(): if not baseline_path.exists(): - problems.append(f'client_baselines/{bridge}.json is missing, so {client} wasn\'t checked live') + problems.append(f'{baseline_path.name} is missing, so {client} wasn\'t checked live') continue baseline = json.loads(baseline_path.read_text()) for page, row in data['errors'].items(): diff --git a/build/test_update_errors.py b/build/test_update_errors.py index ef1d2526e4..ca1d1e04e8 100644 --- a/build/test_update_errors.py +++ b/build/test_update_errors.py @@ -277,9 +277,27 @@ def test_changed_ignores_numbers_and_flags_names(self): self.assertEqual(live_clients.changed(old, same, 'jedis'), []) self.assertEqual([p for p, _ in live_clients.changed(old, diff, 'jedis')], ['moved']) + def test_changed_reports_a_page_that_a_run_lost_or_gained(self): + sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'errors', 'bridges')) + import live_clients + page = {'raw': ['-MOVED 1 h:6570'], 'jedis': ['E: MOVED 1 h:6570']} + old = {'_meta': {}, 'moved': page} + self.assertIn('no replies this time', live_clients.changed(old, {'_meta': {}}, 'jedis')[0][1]) + self.assertIn('no baseline yet', live_clients.changed({'_meta': {}}, old, 'jedis')[0][1]) + def test_the_committed_client_data_matches_the_baselines(self): self.assertEqual(u.check_clients(), []) + def test_a_wrong_redis_py_class_is_a_problem(self): + real = json.loads((u.ROOT / 'data' / 'client-exceptions.json').read_text()) + real['errors']['readonly']['redis-py']['class'] = 'ReadOnlyException' + read_text = u.Path.read_text + with patch.object(u.Path, 'read_text', autospec=True, + side_effect=lambda p, *a, **k: json.dumps(real) if p.name == 'client-exceptions.json' + else read_text(p, *a, **k)): + problems = u.check_clients() + self.assertTrue(any('readonly/redis-py says ReadOnlyException' in p for p in problems), problems) + if __name__ == '__main__': unittest.main()