From c5bf09d3160303bbf2099b596e0c5565e8734149 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Tue, 7 Jul 2026 09:37:41 +0900 Subject: [PATCH 1/3] Throw a clear error when an Execute command contains whitespace Execute("ACL SETUSER x") passes a whole command line as the single command token, which gets sent as one unknown command and comes back as an opaque server error. Since a redis command token never contains internal whitespace, this is always a caller mistake, so the ExecuteMessage constructor now fails fast with an ArgumentException-style RedisCommandException that names the offending command and shows the correct token-per-argument form. Resolves #2689. Signed-off-by: Arpit Jain --- src/StackExchange.Redis/ExceptionFactory.cs | 3 +++ src/StackExchange.Redis/RedisDatabase.cs | 11 ++++++++++ .../AdhocMessageRoundTrip.cs | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index a6e86036b..380ac0c0f 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -30,6 +30,9 @@ internal static Exception CommandDisabled(string command) internal static Exception TooManyArgs(string command, int argCount) => new RedisCommandException($"This operation would involve too many arguments ({argCount + 1} vs the redis limit of {MessageWriter.REDIS_MAX_ARGS}): {command}"); + internal static Exception CommandHasWhitespace(string command) + => new RedisCommandException($"The command '{command}' contains whitespace and would be sent as a single unknown token; pass each word as a separate argument, for example Execute(\"ACL\", \"SETUSER\", \"x\") rather than Execute(\"ACL SETUSER x\")."); + internal static Exception ConnectionFailure(bool includeDetail, ConnectionFailureType failureType, string message, ServerEndPoint? server) { var ex = new RedisConnectionException(failureType, message); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index f06bcc699..8703aebe4 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5549,6 +5549,17 @@ public ExecuteMessage(CommandMap? map, int db, CommandFlags flags, string comman throw ExceptionFactory.TooManyArgs(command, args.Count); } + // a redis command token never contains internal whitespace, so a command like + // "ACL SETUSER x" is always a caller mistake (it gets sent as one unknown token + // and the server replies with an opaque error); fail fast with actionable guidance + foreach (var c in command) + { + if (char.IsWhiteSpace(c)) + { + throw ExceptionFactory.CommandHasWhitespace(command); + } + } + map ??= CommandMap.Default; _unknownCommand = ""; if (Command is RedisCommand.UNKNOWN) diff --git a/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs b/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs index 34d41e884..d1288281f 100644 --- a/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs +++ b/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs @@ -48,6 +48,27 @@ public async Task EchoRoundTripTest(MapMode mode, string payload, string request } } + [Theory(Timeout = 1000)] + [InlineData("ACL SETUSER x")] + [InlineData("get key")] + [InlineData("echo\thello")] + public void CommandWithWhitespaceThrows(string command) + { + object[] args = []; + var ex = Assert.Throws( + () => new RedisDatabase.ExecuteMessage(CommandMap.Default, -1, CommandFlags.None, command, args)); + Assert.Contains("whitespace", ex.Message); + } + + [Fact(Timeout = 1000)] + public void SingleTokenCommandDoesNotThrow() + { + // the correct token-per-argument form must still be accepted unchanged + object[] args = ["SETUSER", "x"]; + var msg = new RedisDatabase.ExecuteMessage(CommandMap.Default, -1, CommandFlags.None, "ACL", args); + Assert.Equal("ACL", msg.CommandString); + } + private static CommandMap? GetMap(MapMode mode) => mode switch { MapMode.Null => null, From ef1edee176709563d65464f11ce234653bc2d0d4 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 10 Jul 2026 11:39:06 +0100 Subject: [PATCH 2/3] test for simple space only limit to simple space; if people are being *that* creative, that's on them --- src/StackExchange.Redis/RedisDatabase.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 8703aebe4..8bb73be4f 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5549,16 +5549,10 @@ public ExecuteMessage(CommandMap? map, int db, CommandFlags flags, string comman throw ExceptionFactory.TooManyArgs(command, args.Count); } - // a redis command token never contains internal whitespace, so a command like + // a redis command token never contains space, so a command like // "ACL SETUSER x" is always a caller mistake (it gets sent as one unknown token // and the server replies with an opaque error); fail fast with actionable guidance - foreach (var c in command) - { - if (char.IsWhiteSpace(c)) - { - throw ExceptionFactory.CommandHasWhitespace(command); - } - } + if (command.IndexOf(' ') >= 0) throw ExceptionFactory.CommandHasWhitespace(command); map ??= CommandMap.Default; _unknownCommand = ""; From 42e811cdcb4ddd98e873922b25595d56922a5f3d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 10 Jul 2026 11:40:23 +0100 Subject: [PATCH 3/3] Remove test case for 'echo\thello' command Removed test case for command with tab character. --- .../RoundTripUnitTests/AdhocMessageRoundTrip.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs b/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs index d1288281f..49daa9a68 100644 --- a/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs +++ b/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs @@ -51,7 +51,6 @@ public async Task EchoRoundTripTest(MapMode mode, string payload, string request [Theory(Timeout = 1000)] [InlineData("ACL SETUSER x")] [InlineData("get key")] - [InlineData("echo\thello")] public void CommandWithWhitespaceThrows(string command) { object[] args = [];