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..8bb73be4f 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5549,6 +5549,11 @@ public ExecuteMessage(CommandMap? map, int db, CommandFlags flags, string comman throw ExceptionFactory.TooManyArgs(command, args.Count); } + // 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 + if (command.IndexOf(' ') >= 0) 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..49daa9a68 100644 --- a/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs +++ b/tests/StackExchange.Redis.Tests/RoundTripUnitTests/AdhocMessageRoundTrip.cs @@ -48,6 +48,26 @@ public async Task EchoRoundTripTest(MapMode mode, string payload, string request } } + [Theory(Timeout = 1000)] + [InlineData("ACL SETUSER x")] + [InlineData("get key")] + 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,